Blog 3

Don’t Just Choose Between Single-Purpose and Lambda-lith: What’s the Benefit of Read-Write Separation (CQRS Prelude)?

Hello everyone. Today, I read an insightful analysis on the AWS Compute Blog by two Principal Solutions Architects regarding serverless microservices design strategies. Since this article directly addresses a common pain point in structuring AWS Lambda functions, I have summarized and added some technical insights here for discussion.

When designing RESTful APIs using AWS Lambda and Amazon API Gateway, the classic question is: How granular should my Lambda functions be?

In practice, developers are often caught between two extremes:

1. Single Responsibility Lambda (One Function per Endpoint)

  • Approach: Maximum decomposition (Fine-grained). Each HTTP route and method (e.g., GET /users, POST /users, DELETE /users/{id}) maps to a dedicated Lambda function.
  • Pros:
    • Complete source code isolation and minimal bundle size, which optimizes the initialization phase (reducing Cold Starts).
    • Strict adherence to the principle of least privilege (IAM). The GET function only requires dynamodb:GetItem permissions, while the POST function requires dynamodb:PutItem.
    • Independent memory/timeout configuration (e.g., assigning 1024MB RAM to heavy computation tasks while leaving lightweight tasks at 128MB).
  • Cons: High Infrastructure as Code (IaC) maintenance overhead. For instance, when using AWS CloudFormation, it is easy to hit the hard limit of 500 resources per stack. Seldom-invoked operations (such as DELETE) frequently have their execution contexts garbage collected, causing high cold start frequencies for end-users. It also violates DRY principles regarding database connection pooling and middleware.

2. The Lambda-lith (Monolithic Lambda)

  • Approach: Bundling all API routes of a service into a single monolithic Lambda function, typically using adapters like aws-serverless-express to run Express/NestJS (Node.js) or Spring Boot (Java) inside Lambda. API Gateway functions purely as a {proxy+} routing layer.
  • Pros: Centralized code repository, simplifying shared DTOs, entities, and database connections. Warm start rates approach 100% since the function continuously handles incoming traffic, keeping the container active.
  • Cons:
    • Bloated Deployment Packages: Unzipped packages easily reach 250MB, causing significant cold start latency—especially when initializing heavy framework dependency injection containers (like NestJS or Spring Boot JVM startup).
    • Wasted Compute Resources: You pay for Lambda compute time (billed in milliseconds) just to perform routing—a task API Gateway can handle much more cost-effectively.
    • Bloated IAM Policies: The monolithic function must have Read/Write permissions to all database tables and S3 buckets accessed by the entire microservice, increasing the blast radius.

The Pragmatic Middle Ground: Read/Write Separation

The AWS authors proposed a third architecture that balances these extremes by separating workloads based on behavior. Instead of over-decomposing or over-consolidating, we divide a bounded context into exactly two Lambda functions:

  1. Command/Write Lambda: Handles all state-changing requests (POST, PUT, DELETE, PATCH).
    • Technical Insight: Write operations share validation logic, ORM mapping libraries, and transaction controls. Bundling them maintains a reasonable package size while allowing less frequent operations (like DELETE) to benefit from the warm execution context generated by high-frequency operations (like POST).
  2. Query/Read Lambda: Handles read-only operations (GET).
    • Technical Insight: Read logic is typically lightweight, executing database queries and mapping responses. It does not require request body validation libraries (such as Joi, Zod, or class-validator). The resulting small bundle size and fast initialization phase optimize API response latency for end-users.

Evolutionary Path to CQRS

The core value of this pattern is providing a seamless transition to CQRS (Command Query Responsibility Segregation) and Event-Driven architectures as the application scales:

  • Decoupled Write Path: Instead of API Gateway calling the Write Lambda synchronously, you can place an Amazon SQS queue in between. API Gateway immediately returns an HTTP 202 Accepted response. The Write Lambda then pulls messages from SQS via Event Source Mapping in batches, utilizing Dead Letter Queues (DLQ) for retries. This shields relational databases (like RDS) from connection exhaustion during traffic spikes.
  • Scalable Read Path: Since read logic is completely isolated, you can freely implement caching layers (like ElastiCache/Redis using the Cache-Aside pattern) or route the Read Lambda to a read replica database without risking write path side effects.

Real-World Use Case: Education Technology (EdTech) Platform

Consider a School Management System. Traffic between read and write operations is highly asymmetric. Read requests (students viewing schedules, checking grades, downloading announcements) can generate thousands of requests per second (RPS). In contrast, write requests (teachers taking attendance, entering grades) are far less frequent but require strict ACID transaction properties.

Applying the Read/Write Separation pattern allows you to independently scale memory and concurrency for read paths during high-demand exam periods while keeping the write paths isolated, cost-optimized, and secure.

Conclusion

In serverless architectures, there is no silver bullet. The selection of an architecture depends on the specific business context, balancing development speed (DevX), cost, security (IAM), and performance (Cold Starts).


Original Source: Comparing design approaches for building serverless microservices | AWS Compute Blog

Translated Post: AWS Study Group VN | Facebook

Illustrations:

Single Responsibility vs Monolithic Lambda Comparison Diagram Read/Write Separation Diagram