CQRS on AWS: Lambda + DynamoDB + Streams + OpenSearchCloud Architecture Patterns & Workflows

By | December 25, 2025

What you’ll learn

  • What CQRS is and why it exists
  • How to implement CQRS on AWS using standard serverless components
  • How the write model and read model stay in sync (and what “eventual consistency” means in practice)
  • Key production considerations: failures, retries, idempotency, monitoring, and security

The problem CQRS solves

In many systems, we try to make one database schema do everything:

  • Handle writes with strong correctness rules (validation, constraints, transactions)
  • Serve complex reads (search, filtering, aggregations, “feed” style queries)

As the system grows, that single model starts to hurt:

  • Writes become complicated because the schema is optimized for read queries
  • Reads become slow because the schema is optimized for updates and integrity
  • Every new feature adds more indexes, more joins, more hot partitions, or more caching hacks

CQRS (Command Query Responsibility Segregation) is a pattern that says:

  • Commands (writes) and Queries (reads) have different jobs
  • So it’s often beneficial to give them different models, and sometimes different datastores

CQRS in one sentence

CQRS separates the write path (commands) from the read path (queries) and keeps a read-optimized model in sync with the write model via asynchronous events.

In this AWS version:

  • Write path: API Gateway → Lambda → DynamoDB (write model)
  • Read path: DynamoDB Streams → Lambda → Amazon OpenSearch Service (read model)

Note: In many older diagrams, you’ll see “Elasticsearch”—on AWS, the managed service is now Amazon OpenSearch Service.


High-level architecture (AWS components)

What each component is responsible for

  • API Gateway: public HTTP endpoints (rate limiting, auth integration, request validation if desired)
  • Command Lambda: validates commands and writes the source of truth to DynamoDB
  • DynamoDB (write model): authoritative state; optimized for writes and correctness rules
  • DynamoDB Streams: reliable change feed whenever DynamoDB items change
  • Projector Lambda: transforms DynamoDB changes into read-optimized documents
  • OpenSearch (read model): optimized for search, filtering, sorting, aggregations, “feed” queries
  • Query Lambda: serves fast queries from OpenSearch (and can enforce authorization)
  • DLQ (SQS): catches failed projections so you don’t lose updates silently
  • CloudWatch/X-Ray: logs, metrics, alarms, tracing across the flow

A concrete example to explain it (simple “Orders” domain)

Imagine an e-commerce service.

Commands (write intent)

  • CreateOrder(orderId, customerId, items…)
  • ConfirmPayment(orderId, paymentId)
  • CancelOrder(orderId, reason)

These commands must enforce business rules:

  • Don’t confirm payment twice
  • Don’t cancel a shipped order
  • Validate inventory or price snapshots

Queries (read intent)

  • “Show me all orders for a customer, newest first”
  • “Search orders by product name”
  • “Give me daily revenue totals”

Those are typically easier and faster in a search/analytics-friendly store like OpenSearch.


The write path (Command side) — how data is written

The command side focuses on correctness and business rules.

Practical AWS flow:

  1. Client calls POST /orders (or PUT /orders/{id})
  2. API Gateway forwards to Command Lambda
  3. Lambda validates the request and applies business logic
  4. Lambda writes to DynamoDB (often with conditional writes for correctness)
  5. DynamoDB emits a change record to DynamoDB Streams

Command side sequence (what happens, in order)


The read path (Query side) — how data is read fast

The query side focuses on speed and flexible access patterns.

Practical AWS flow:

  1. DynamoDB Streams triggers the Projector Lambda
  2. Projector transforms the change into one or more read documents
  3. Projector writes/upserts into OpenSearch
  4. Client calls GET /orders?customerId=…
  5. Query Lambda reads from OpenSearch and returns results quickly

Projection + query sequence (and where eventual consistency appears)

What “eventual consistency” means here

Immediately after a command succeeds, the read model might still be catching up for a short time (milliseconds to seconds, depending on load, retries, and indexing).

For most product experiences, that’s fine. When it’s not fine, you can:

  • Read-your-own-write on the command side (return the computed state from DynamoDB for that request)
  • Show UI states like “Processing…” until the query model reflects the change
  • Provide an endpoint that reads directly from the write model for critical screens

Why DynamoDB + Streams + OpenSearch is a standard CQRS combo

  • DynamoDB is great for high-throughput writes, conditional updates, and predictable scaling
  • Streams gives you a durable change feed without running your own Kafka cluster
  • OpenSearch excels at search, filters, faceted navigation, text queries, and aggregations

This combo is handy for:

  • Search-heavy apps (marketplaces, content platforms)
  • Event timelines and activity feeds
  • Admin dashboards with flexible filtering

Production checklist (the stuff that makes CQRS work reliably)

1) Idempotency on the projector

Streams can deliver retries; OpenSearch operations should be idempotent.

  • Use deterministic document IDs (for example, orderId)
  • For deletes, handle REMOVE events cleanly

2) Error handling with DLQ + replay strategy

If the projection fails, you need a recovery plan:

  • Configure Lambda destinations or an SQS DLQ
  • Log enough context to reprocess
  • Keep a “rebuild read model” job in your back pocket (re-scan DynamoDB and re-index)

3) Ordering and concurrency assumptions

Streams preserve ordering per partition key, but at scale you still need to design for:

  • Out-of-order updates across different keys
  • Parallel batches in Lambda

4) Data modeling: keep the read model purpose-built

Your OpenSearch document can be denormalized:

  • Include customer name, totals, status, timestamps, and searchable fields
  • Precompute what you need for query performance (within reason)

5) Observability

Minimum baseline:

  • CloudWatch alarms on projector errors, DLQ depth, and OpenSearch write failures
  • A dashboard for stream iterator age/lag (how far behind projections are)

6) Security

Good defaults:

  • IAM least privilege between Lambdas, DynamoDB, and OpenSearch
  • KMS encryption for DynamoDB and OpenSearch
  • If OpenSearch is VPC-only, ensure Lambdas run in VPC with correct networking
  • Use Cognito (or a JWT authorizer) for user-facing auth

When CQRS is a great fit (and when it’s overkill)

Great fit:

  • Read patterns are complex and change frequently
  • You need search/analytics queries without impacting writes
  • You can tolerate brief eventual consistency on the read side

Overkill:

  • A single database model can handle your load and query needs
  • Your domain requires strict read-after-write consistency everywhere
  • Your team is small, and operational simplicity is the top priority

Optional upgrades (nice to mention)

  • Replace Streams→Projector with EventBridge or Kinesis for more complex fan-out
  • Use Aurora/RDS as the write model when transactions/joins are required
  • Add Step Functions when commands become multi-step workflows
  • Split into microservices, where each service owns its own command and query models