After two decades designing distributed systems, there's one lesson that keeps repeating itself: scalability problems are rarely solved with more compute. They're solved with better patterns. On AWS, three patterns account for most of the architectural value I've seen work in production: CQRS, Event Sourcing, and well-bounded microservices decomposition.

CQRS: separating reads from writes

Command Query Responsibility Segregation separates the model that writes data from the model that reads it. In systems with read loads far higher than write loads—the typical case for dashboards, catalogs, or reports—this allows each side to scale independently.

A common AWS implementation combines DynamoDB as the transactional store for commands with OpenSearch or a materialized view in RDS for complex queries:

// Comando: escribe en DynamoDB como fuente de verdad
export async function createOrder(command: CreateOrderCommand) {
  await dynamoClient.send(
    new PutItemCommand({
      TableName: 'Orders',
      Item: marshall({ ...command, status: 'PENDING', createdAt: Date.now() }),
    })
  )
}

// Proyección: un stream de DynamoDB alimenta la vista de lectura
export async function projectOrderToReadModel(record: DynamoDBRecord) {
  const order = unmarshall(record.dynamodb.NewImage)
  await openSearchClient.index({
    index: 'orders-read-model',
    id: order.id,
    body: order,
  })
}

The cost of this pattern is eventual consistency: the read view updates with a lag of milliseconds. In most business domains that cost is acceptable; in financial domains with strong consistency requirements, it isn't, and in those cases I prefer a single transactional model.

Event Sourcing: history as the source of truth

Instead of persisting the current state of an entity, Event Sourcing persists the sequence of events that led to that state. The state is derived by replaying the events. On AWS this is naturally implemented with Kinesis Data Streams or EventBridge as the backbone, and DynamoDB or S3 as an immutable event store.

The real benefit isn't technical, it's business-related: when the full history exists, questions like "why did this order end up in this state?" no longer require log archaeology. It also enables rebuilding new projections without migrating data, simply by replaying the stream against a new consumer.

The trade-off is operational complexity: versioning events, handling snapshots to avoid replaying entire histories, and designing idempotent consumers. I reserve it for domains where auditability is an explicit business requirement, not a "nice to have."

Microservices: the boundary matters more than the size

The most common mistake I see in AWS microservices migrations isn't making services "too large"; it's drawing boundaries by technical layer instead of by business domain. A "users" service that also handles authentication, preferences, and billing isn't a microservice, it's a monolith with more network hops.

On AWS, the pattern that has worked best in my projects combines:

  • API Gateway as a single entry point, with centralized authorization.
  • ECS Fargate or Lambda per service, depending on the load profile (Fargate for sustained loads, Lambda for sporadic events or spikes).
  • SQS/SNS or EventBridge for asynchronous communication between services, avoiding chained synchronous calls that propagate failures.
  • An API Gateway with versioned contracts so each service can evolve without breaking its consumers.
# Ejemplo simplificado de una definición SAM para un servicio de dominio
Resources:
  OrdersFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: dist/handler.main
      Runtime: nodejs20.x
      Events:
        CreateOrder:
          Type: Api
          Properties:
            Path: /orders
            Method: post
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref OrdersTable

The rule I always apply

No pattern is free. CQRS adds eventual consistency, Event Sourcing adds replay complexity, microservices add network latency and operational surface. The right question is never "is this pattern modern?", but rather "does the problem I have justify the cost this pattern introduces?". Mature architecture isn't the one that uses the most patterns; it's the one that uses the fewest necessary for the actual problem.