
Building an Event-Driven Order Processing System with AWS Lambda, SQS, and DynamoDB
In this article, we will explore how to build a scalable, asynchronous, event-driven order-processing architecture using AWS Lambda, Amazon SQS, and Amazon DynamoDB. We will cover the architecture, event flow, implementation approach, failure handling, idempotency, scalability, security, monitoring, and production best practices.
1. Introduction
Imagine that we are building an e-commerce platform.
When a customer clicks Place Order, the application needs to perform several operations:
- Validate the request
- Create the order
- Store order information
- Process the order
- Update its status
- Handle failures
- Potentially trigger other services such as payment, inventory, shipping, and notifications
A simple architecture might call every service directly:
Customer | vOrder API | +----> Payment Service | +----> Inventory Service | +----> Notification Service | +----> Shipping ServiceThis works for smaller systems, but it creates tight coupling.
Suppose the notification service becomes unavailable. Should that prevent the customer's order from being accepted?
Probably not.
Instead, we can introduce asynchronous messaging.
Customer | vAmazon API Gateway | vOrder Lambda | vAmazon SQS | vOrder Processing Lambda | vAmazon DynamoDBThe API can accept the request quickly while processing takes place asynchronously.
This is one of the core ideas behind event-driven architecture.
2. What Is Event-Driven Architecture?
Event-driven architecture is an architectural approach in which components communicate by producing and consuming events.
An event represents something that has happened in the system.
For example:
{ "eventType": "ORDER_CREATED", "orderId": "ORD-10001", "customerId": "CUS-501", "amount": 149.99, "createdAt": "2026-09-28T10:30:00Z"}Instead of one service waiting for another service to finish, the producer publishes a message or event.
Another component processes it independently.
This creates loose coupling between different parts of the application.
3. AWS Services Used
Our architecture uses several AWS serverless services.
Amazon API Gateway
Amazon API Gateway exposes an HTTP endpoint to clients.
For example:
POST /ordersThe client sends the order request to this endpoint.
API Gateway then invokes the order-creation Lambda function.
AWS Lambda
AWS Lambda allows us to execute application logic without provisioning or managing traditional application servers.
In this architecture, we can have functions such as:
CreateOrderFunctionProcessOrderFunctionThe first function receives the API request.
The second function consumes messages from Amazon SQS.
Amazon SQS
Amazon Simple Queue Service acts as a buffer between our order producer and order processor.
Instead of directly invoking the processing service, the application places an order message onto a queue.
For example:
order-processing-queueThe message waits there until the processing Lambda can handle it.
This helps isolate the producer from downstream processing failures and allows processing capacity to scale independently.
Amazon DynamoDB
Amazon DynamoDB stores our order information.
An order might look like:
{ "orderId": "ORD-10001", "customerId": "CUS-501", "status": "PROCESSING", "amount": 149.99, "createdAt": "2026-09-28T10:30:00Z"}For a simple order model,
orderId could be the partition key.For production systems, your DynamoDB key and index design should follow your application's access patterns rather than simply copying a relational database schema.
4. Architecture
The basic architecture looks like this:
┌──────────────────┐ │ Customer │ └────────┬─────────┘ │ POST /orders │ ▼ ┌──────────────────┐ │ API Gateway │ └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ Create Order │ │ Lambda │ └────────┬─────────┘ │ Send Message │ ▼ ┌──────────────────┐ │ Amazon SQS │ │ Order Queue │ └────────┬─────────┘ │ Event Source │ ▼ ┌──────────────────┐ │ Process Order │ │ Lambda │ └────────┬─────────┘ │ Put/Update Item │ ▼ ┌──────────────────┐ │ Amazon DynamoDB │ │ Orders Table │ └──────────────────┘For production use, we should also introduce a dead-letter queue:
Order Queue | vProcessing Lambda | | repeated failure vDead-Letter Queue
5. Complete Order Flow
Let's walk through the architecture.
Step 1 — Customer Places an Order
The client sends:
POST /ordersContent-Type: application/jsonExample request:
{ "customerId": "CUS-501", "items": [ { "productId": "PROD-101", "quantity": 2, "price": 49.99 }, { "productId": "PROD-205", "quantity": 1, "price": 50.01 } ]}
Step 2 — API Gateway Invokes Lambda
API Gateway forwards the request to:
CreateOrderFunctionThe function validates the request and generates identifiers such as:
orderIdeventIdFor example:
{ "eventId": "evt-93832", "eventType": "ORDER_CREATED", "orderId": "ORD-10001", "customerId": "CUS-501", "totalAmount": 149.99}
6. Publishing the Order to SQS
The Lambda function publishes the message to our SQS queue.
A simplified Node.js example using AWS SDK for JavaScript v3 could look like:
import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs";import crypto from "node:crypto";
const sqs = new SQSClient({});
export const handler = async (event) => { const request = JSON.parse(event.body);
const orderId = crypto.randomUUID(); const eventId = crypto.randomUUID();
const orderEvent = { eventId, eventType: "ORDER_CREATED", orderId, customerId: request.customerId, items: request.items, createdAt: new Date().toISOString() };
await sqs.send( new SendMessageCommand({ QueueUrl: process.env.ORDER_QUEUE_URL, MessageBody: JSON.stringify(orderEvent) }) );
return { statusCode: 202, headers: { "content-type": "application/json" }, body: JSON.stringify({ orderId, status: "ACCEPTED" }) };};Notice the HTTP status:
202 AcceptedThat communicates an important idea: the request was accepted, but asynchronous processing may still be running.
7. Why Put SQS Between the Lambda Functions?
You might wonder why we don't simply call the second Lambda directly.
The queue provides a useful decoupling boundary.
Without SQS:
Lambda A | vLambda BWith SQS:
Lambda A | v SQS | vLambda BThis provides several advantages.
Loose coupling
The producer doesn't need to wait for the consumer to finish.
Failure isolation
If processing temporarily fails, messages can remain in the queue and be retried.
Traffic buffering
A sudden increase in incoming orders doesn't require every order to be processed at exactly the same moment.
Independent scaling
The producer and consumer can scale independently.
This is particularly useful when incoming traffic is bursty.
8. Lambda Consumes Messages from SQS
AWS Lambda can use an SQS queue as an event source.
The processing function receives records in batches.
A simplified example:
import { DynamoDBClient} from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, PutCommand} from "@aws-sdk/lib-dynamodb";
const client = new DynamoDBClient({});const dynamodb = DynamoDBDocumentClient.from(client);
export const handler = async (event) => { for (const record of event.Records) { const order = JSON.parse(record.body);
await dynamodb.send( new PutCommand({ TableName: process.env.ORDERS_TABLE, Item: { orderId: order.orderId, customerId: order.customerId, items: order.items, status: "PROCESSED", createdAt: order.createdAt, processedAt: new Date().toISOString() } }) ); }};AWS manages polling the queue and invoking the function.
9. Understanding At-Least-Once Processing
This is one of the most important concepts in the architecture.
An SQS-triggered Lambda should not assume that every message will be processed exactly once.
AWS states that Lambda event-source mappings process events at least once, so duplicate processing can occur. AWS therefore recommends making functions idempotent.
Imagine this sequence:
SQS | vLambda | +---- Process order successfully | +---- Write to DynamoDB | X Function fails before completion is acknowledgedThe message might later be delivered again.
If our application blindly processes the order twice, we could potentially:
- Charge a customer twice
- Reduce inventory twice
- Send duplicate notifications
- Create duplicate records
Therefore, our consumer must be idempotent.
10. Implementing Idempotency
Idempotency means:
Processing the same event multiple times should produce the same effective result as processing it once.
One approach is to attach a unique identifier to every event:
{ "eventId": "evt-93832", "eventType": "ORDER_CREATED", "orderId": "ORD-10001"}The consumer can store processed event identifiers.
Conceptually:
Receive event | vHas eventId already been processed? | +--+--+ YES NO | |Ignore Process | v Record eventIdDynamoDB conditional writes can be useful for implementing this type of protection.
AWS also recommends idempotent Lambda functions and notes that DynamoDB can be used to track processed identifiers, often with TTL to eventually expire those records.
11. Handling Failed Messages
Not every order will process successfully.
For example:
Invalid orderDependency unavailableUnexpected application errorDatabase problemMalformed messageTransient failures should usually be retried.
But continuously retrying a permanently invalid message is undesirable.
This is where a Dead-Letter Queue (DLQ) becomes useful.
┌───────────────────┐ │ Order SQS Queue │ └─────────┬─────────┘ │ ▼ ┌───────────────────┐ │ Processing Lambda │ └─────────┬─────────┘ │ Failure │ Retry │ repeated failure │ ▼ ┌───────────────────┐ │ DLQ │ └───────────────────┘AWS recommends configuring a DLQ for failed SQS messages and suggests a
maxReceiveCount of at least five as a starting recommendation for Lambda/SQS configurations. The appropriate value should still reflect your application's failure and recovery characteristics.The DLQ gives operators somewhere to inspect messages that repeatedly fail instead of allowing them to circulate indefinitely.
12. Partial Batch Failure
Suppose Lambda receives five SQS messages:
Order 1 -> SuccessOrder 2 -> SuccessOrder 3 -> FailureOrder 4 -> SuccessOrder 5 -> SuccessWithout partial batch failure handling, one failure can cause successfully processed messages in the batch to become available again.
AWS supports:
ReportBatchItemFailuresThis allows the consumer to report which individual messages failed so only those records need to be retried.
Conceptually:
{ "batchItemFailures": [ { "itemIdentifier": "failed-message-id" } ]}This can significantly reduce unnecessary duplicate processing.
13. Configure the SQS Visibility Timeout Correctly
Another important production configuration is the visibility timeout.
After Lambda receives an SQS message, that message becomes temporarily invisible to other consumers.
AWS recommends configuring the queue visibility timeout to at least six times the Lambda function timeout, with additional consideration for any configured batching window.
For example, if:
Lambda timeout = 30 secondsa starting visibility-timeout configuration would be:
30 × 6 = 180 secondsYour actual settings should reflect processing time, retries, batching, and workload characteristics.
14. DynamoDB Data Model
A simple Orders table could use:
Table: Orders
Partition Key:orderIdExample:
{ "orderId": "ORD-10001", "customerId": "CUS-501", "status": "PROCESSED", "totalAmount": 149.99, "createdAt": "2026-09-28T10:30:00Z", "processedAt": "2026-09-28T10:30:04Z"}But production DynamoDB design should begin with access patterns.
For example:
Get an order by IDGet all orders for a customerFind orders by statusFind recent ordersYou may therefore need additional key structures or Global Secondary Indexes.
AWS DynamoDB guidance recommends high-cardinality partition keys to distribute load and careful use of GSIs based on required access patterns.
15. Scaling the Architecture
One major benefit of this design is its ability to absorb changing traffic.
Imagine normal traffic:
10 orders/secondDuring a major promotion:
1,000 orders/secondSQS can buffer messages while Lambda processes them.
Conceptually:
Customers | | sudden traffic spike vAPI Gateway | vLambda | vSQS Queue||||||||||||||||||||||| | | controlled consumption vProcessing Lambda | vDynamoDBThis prevents the entire processing chain from requiring the same instantaneous throughput as the incoming API.
Lambda's SQS event source mapping also provides batching and concurrency controls that can be tuned for the workload.
16. Security Considerations
Production architectures should follow the principle of least privilege.
The order-creation Lambda might only need permission to:
sqs:SendMessageThe processing Lambda might need operations such as:
dynamodb:PutItemdynamodb:UpdateItemdynamodb:GetItemAvoid unnecessarily broad permissions such as:
Action: "*"Resource: "*"Each function should receive only the permissions required to perform its responsibilities.
Sensitive configuration should also be handled appropriately rather than hard-coded into source code.
17. Monitoring and Observability
Building the system is only part of the work.
We also need to understand what it is doing in production.
Amazon CloudWatch can be used to monitor Lambda and SQS behavior.
Useful signals include:
Lambda errorsLambda durationLambda throttlesSQS queue depthAge of the oldest SQS messageDLQ message countDynamoDB throttlingA particularly useful scenario to detect is:
Incoming orders | vSQS queue growing | vOldest message age increasing | vConsumer cannot keep upCloudWatch alarms can notify operators when these signals cross appropriate thresholds.
18. A More Production-Ready Architecture
Our initial architecture can evolve further.
Internet | v ┌───────────────┐ │ API Gateway │ └───────┬───────┘ | v ┌───────────────┐ │ Create Order │ │ Lambda │ └───────┬───────┘ | v ┌───────────────┐ │ Amazon SQS │ │ Order Queue │ └───────┬───────┘ | v ┌───────────────┐ │Process Order │ │ Lambda │ └───────┬───────┘ | v ┌───────────────┐ │ DynamoDB │ │ Orders Table │ └───────────────┘
Failure path:
Order Queue | repeated failure | v┌────────────────┐│ DLQ │└────────────────┘
Observability:
Lambda + SQS + DynamoDB | v CloudWatch | v Metrics / Logs | v Alarms
19. Important Production Problem: The Dual-Write Problem
There is another issue worth understanding.
Imagine that your application does this:
1. Save order in DynamoDB2. Send ORDER_CREATED to SQSWhat happens if step 1 succeeds but step 2 fails?
DynamoDB = order exists
SQS = event missingThe database and event-processing pipeline are now inconsistent.
The opposite ordering creates another failure scenario.
This is known as the dual-write problem.
AWS Prescriptive Guidance discusses the transactional outbox pattern as a solution to this class of problem. For DynamoDB-based architectures, DynamoDB Streams can capture item-level changes, allowing a Lambda consumer to publish downstream notifications without requiring the original request path to independently coordinate a database write and message publication.
A more advanced design could therefore become:
API Gateway | vCreate Order Lambda | vDynamoDB | vDynamoDB Streams | vEvent Processing Lambda | vSQS | vOrder Processing ServicesThis is an excellent evolution path when stronger reliability guarantees are required.
20. SQS Standard vs FIFO
For many workloads, an SQS Standard queue is appropriate.
However, some order-processing workflows may require stricter ordering.
For example:
ORDER_CREATED | vPAYMENT_CONFIRMED | vORDER_SHIPPEDIf message ordering is important, consider whether an SQS FIFO queue better matches the business requirements.
The decision should be based on ordering, throughput, deduplication, and application semantics rather than automatically choosing one queue type for every workload.
21. Why This Architecture Works Well
The architecture combines several useful cloud-native characteristics.
Scalability
Lambda and DynamoDB can scale with workload, while SQS absorbs bursts between producers and consumers.
Loose coupling
The API-facing producer doesn't need to know exactly how downstream order processing is implemented.
Resilience
Temporary consumer failures don't necessarily prevent new messages from being accepted into the queue.
Asynchronous processing
Longer-running downstream operations don't need to increase the latency of the initial HTTP request.
Operational visibility
CloudWatch metrics, logs, alarms, and DLQs give operators tools for identifying and investigating problems.
Cost model
These managed/serverless services can be attractive for workloads with changing traffic because infrastructure does not need to remain permanently provisioned solely for peak demand.
22. Key Lessons
When designing an event-driven system with Lambda, SQS, and DynamoDB, remember these principles:
- Design for duplicate delivery. Make consumers idempotent.
- Use a DLQ. Don't allow permanently failing messages to retry indefinitely.
- Use partial batch responses. Retry failed messages rather than unnecessarily reprocessing successful ones.
- Configure visibility timeout carefully. Processing and retry behavior depend on it.
- Design DynamoDB around access patterns. Don't simply reproduce relational-table thinking.
- Apply least-privilege IAM permissions.
- Monitor queue depth, message age, Lambda errors, throttling, and DLQs.
- Understand the dual-write problem. Consider DynamoDB Streams or an appropriate transactional-outbox-style architecture when database changes must reliably trigger downstream events.
- Design for failure from the beginning. Distributed systems will experience transient failures and duplicate deliveries.
- Keep services loosely coupled. One component's temporary failure should not unnecessarily bring down the entire workflow.
Conclusion
Event-driven architecture changes the way we think about backend systems.
Instead of building one large synchronous chain:
Request -> Service A -> Service B -> Service C -> Responsewe can design independent components that communicate asynchronously:
Producer | vEvent / Message | vConsumerFor an order-processing system, AWS Lambda, Amazon SQS, and Amazon DynamoDB provide a powerful foundation for this architecture.
Lambda executes business logic without traditional server management.
SQS provides asynchronous buffering and decoupling.
DynamoDB provides scalable persistence.
Combined with idempotency, dead-letter queues, partial batch failure handling, monitoring, least-privilege IAM, and careful failure design, these services can form the basis of a resilient production architecture.
More importantly, this architecture introduces a broader engineering principle:
Don't design distributed systems assuming that nothing will fail. Design them so that individual failures can occur without breaking the entire system.
That principle is at the heart of building reliable event-driven applications on AWS.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article