AWS Builder Center
Building an Event-Driven Order Processing System with AWS Lambda, SQS, and DynamoDB

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
|
v
Order API
|
+----> Payment Service
|
+----> Inventory Service
|
+----> Notification Service
|
+----> Shipping Service
This 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
|
v
Amazon API Gateway
|
v
Order Lambda
|
v
Amazon SQS
|
v
Order Processing Lambda
|
v
Amazon DynamoDB
The 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 /orders
The 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:
CreateOrderFunction
ProcessOrderFunction
The 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-queue
The 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
|
v
Processing Lambda
|
| repeated failure
v
Dead-Letter Queue



5. Complete Order Flow

Let's walk through the architecture.

Step 1 — Customer Places an Order

The client sends:
POST /orders
Content-Type: application/json
Example 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:
CreateOrderFunction
The function validates the request and generates identifiers such as:
orderId
eventId
For 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 Accepted
That 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
|
v
Lambda B
With SQS:
Lambda A
|
v
SQS
|
v
Lambda B
This 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
|
v
Lambda
|
+---- Process order successfully
|
+---- Write to DynamoDB
|
X Function fails before completion is acknowledged
The 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
|
v
Has eventId already been processed?
|
+--+--+
YES NO
| |
Ignore Process
|
v
Record eventId
DynamoDB 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 order
Dependency unavailable
Unexpected application error
Database problem
Malformed message
Transient 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 -> Success
Order 2 -> Success
Order 3 -> Failure
Order 4 -> Success
Order 5 -> Success
Without partial batch failure handling, one failure can cause successfully processed messages in the batch to become available again.
AWS supports:
ReportBatchItemFailures
This 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 seconds
a starting visibility-timeout configuration would be:
30 × 6 = 180 seconds
Your 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:
orderId
Example:
{
"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 ID
Get all orders for a customer
Find orders by status
Find recent orders
You 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/second
During a major promotion:
1,000 orders/second
SQS can buffer messages while Lambda processes them.
Conceptually:
Customers
|
| sudden traffic spike
v
API Gateway
|
v
Lambda
|
v
SQS Queue
|||||||||||||||||||||||
|
| controlled consumption
v
Processing Lambda
|
v
DynamoDB
This 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:SendMessage
The processing Lambda might need operations such as:
dynamodb:PutItem
dynamodb:UpdateItem
dynamodb:GetItem
Avoid 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 errors
Lambda duration
Lambda throttles
SQS queue depth
Age of the oldest SQS message
DLQ message count
DynamoDB throttling
A particularly useful scenario to detect is:
Incoming orders
|
v
SQS queue growing
|
v
Oldest message age increasing
|
v
Consumer cannot keep up
CloudWatch 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 DynamoDB
2. Send ORDER_CREATED to SQS
What happens if step 1 succeeds but step 2 fails?
DynamoDB = order exists

SQS = event missing
The 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
|
v
Create Order Lambda
|
v
DynamoDB
|
v
DynamoDB Streams
|
v
Event Processing Lambda
|
v
SQS
|
v
Order Processing Services
This 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
|
v
PAYMENT_CONFIRMED
|
v
ORDER_SHIPPED
If 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:
  1. Design for duplicate delivery. Make consumers idempotent.
  1. Use a DLQ. Don't allow permanently failing messages to retry indefinitely.
  1. Use partial batch responses. Retry failed messages rather than unnecessarily reprocessing successful ones.
  1. Configure visibility timeout carefully. Processing and retry behavior depend on it.
  1. Design DynamoDB around access patterns. Don't simply reproduce relational-table thinking.
  1. Apply least-privilege IAM permissions.
  1. Monitor queue depth, message age, Lambda errors, throttling, and DLQs.
  1. Understand the dual-write problem. Consider DynamoDB Streams or an appropriate transactional-outbox-style architecture when database changes must reliably trigger downstream events.
  1. Design for failure from the beginning. Distributed systems will experience transient failures and duplicate deliveries.
  1. 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 -> Response
we can design independent components that communicate asynchronously:
Producer
|
v
Event / Message
|
v
Consumer
For 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.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article