| 1 | Overview | 2 | Architecture Overview |
| 3 | AWS Service Topology | 4 | Implementation Guide |
| 5 | Decision Criteria | 6 | Cost Model |
| 7 | Anti-Patterns to Avoid | 8 | References |
Overview
In an event-driven architecture, services communicate by publishing events to a bus or queue rather than calling each other directly. The producing service does not know who consumes its events, and consumers do not know who produces them. This inversion of control eliminates the tight coupling that makes monolithic and synchronous microservice systems fragile at scale.
AWS implements this pattern using EventBridge as the central event bus, SQS for reliable point-to-point delivery with guaranteed processing, SNS for fan-out to multiple consumers simultaneously, and Lambda as the serverless compute layer that processes events without managing servers.
Architecture Overview
The conceptual pattern separates the architecture into three zones: producers that emit events, the event routing layer that filters and routes based on content, and consumers that process events independently and idempotently.
%%{init:{'theme':'base','themeVariables':{'fontSize':'14px','fontFamily':'IBM Plex Sans, system-ui, sans-serif','primaryColor':'#DBEAFE','primaryTextColor':'#1e3a5f','primaryBorderColor':'#2563EB','lineColor':'#374151','clusterBkg':'#F9FAFB','clusterBorder':'#D1D5DB','edgeLabelBackground':'#FFFFFF'},'flowchart':{'curve':'orthogonal','padding':30,'nodeSpacing':65,'rankSpacing':75,'useMaxWidth':true}}}%% flowchart TD START([Business Event Occurs]) subgraph PRODUCERS["Event Producers"] P1[Order Service] P2[Payment Service] P3[Inventory Service] end subgraph ROUTING["Event Routing Layer"] BUS[Event Bus\nContent-based routing\nSchema validation] RULE1{Order Events} RULE2{Payment Events} RULE3{Inventory Events} end subgraph CONSUMERS["Event Consumers — Independent and Idempotent"] C1[Notification Service\nCustomer alerts] C2[Analytics Service\nBusiness intelligence] C3[Audit Service\nCompliance logging] C4[Fulfillment Service\nWarehouse trigger] end DLQ[Dead Letter Queue\nFailed event storage\nReplay capability] END([System State Updated — No Direct Coupling]) START --> P1 & P2 & P3 P1 & P2 & P3 -->|Publish event| BUS BUS --> RULE1 & RULE2 & RULE3 RULE1 -->|Route| C1 & C2 & C4 RULE2 -->|Route| C2 & C3 RULE3 -->|Route| C4 C1 & C2 & C3 & C4 -->|On failure| DLQ C1 & C2 & C3 & C4 --> END style START fill:#4f8ef7,color:#fff style END fill:#10b981,color:#fff style DLQ fill:#fef3c7 style BUS fill:#e0f2fe
AWS Service Topology
On AWS, the event-driven pattern uses specific managed services at each layer. EventBridge provides content-based routing with event rules and schema enforcement. SQS provides the reliable delivery queue between EventBridge and Lambda, absorbing traffic spikes and enabling retry without data loss. Lambda processes each event without managing servers.
%%{init:{'theme':'base','themeVariables':{'fontSize':'14px','fontFamily':'IBM Plex Sans, system-ui, sans-serif','primaryColor':'#DBEAFE','primaryTextColor':'#1e3a5f','primaryBorderColor':'#2563EB','lineColor':'#374151','clusterBkg':'#F9FAFB','clusterBorder':'#D1D5DB','edgeLabelBackground':'#FFFFFF'},'flowchart':{'curve':'orthogonal','padding':30,'nodeSpacing':65,'rankSpacing':75,'useMaxWidth':true}}}%% flowchart TD SOURCE[Application or AWS Service\nPuts event to EventBridge] subgraph EB["Amazon EventBridge"] CUSTOM[Custom Event Bus\nIsolates domain events] SCHEMA[Schema Registry\nOpenAPI 3.0 schemas\nCode binding generation] RULE[Event Rules\nContent-based filtering\nTransformation with InputPath] end subgraph QUEUE["Amazon SQS — Reliable Delivery"] SQS_STD[Standard Queue\nAt-least-once delivery\nHigh throughput] SQS_FIFO[FIFO Queue\nExactly-once processing\nOrdered per group] DLQ_Q[Dead Letter Queue\nMaxReceiveCount: 3\nCloudWatch alarm on depth] end subgraph COMPUTE["AWS Lambda — Serverless Processing"] FN1[Order Processor\nIdempotency key check\nDynamoDB conditional write] FN2[Notification Sender\nSES or SNS publish\nTemplate rendering] FN3[Audit Logger\nAppend-only write\nOpenSearch indexing] end subgraph STORE["State and Storage"] DDB[Amazon DynamoDB\nEvent state\nIdempotency table] S3_STORE[Amazon S3\nEvent archive\nAthena queryable] end SNS_FAN[Amazon SNS\nFan-out topic\nMultiple SQS subscribers] SOURCE --> CUSTOM CUSTOM --> SCHEMA SCHEMA --> RULE RULE -->|Single consumer| SQS_STD & SQS_FIFO RULE -->|Fan-out| SNS_FAN SNS_FAN --> SQS_STD SQS_STD --> FN1 & FN2 SQS_FIFO --> FN3 SQS_STD & SQS_FIFO -->|On failure| DLQ_Q FN1 --> DDB FN2 & FN3 --> S3_STORE style SOURCE fill:#4f8ef7,color:#fff style DLQ_Q fill:#fef3c7 style SCHEMA fill:#e0f2fe style SNS_FAN fill:#e0f2fe
Implementation Guide
CDK Stack — Event-Driven Order Processing
import * as cdk from 'aws-cdk-lib';
import * as events from 'aws-cdk-lib/aws-events';
import * as targets from 'aws-cdk-lib/aws-events-targets';
import * as sqs from 'aws-cdk-lib/aws-sqs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as lambdaEventSources from 'aws-cdk-lib/aws-lambda-event-sources';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import { Duration, RemovalPolicy } from 'aws-cdk-lib';
export class EventDrivenStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Idempotency table
const idempotencyTable = new dynamodb.Table(this, 'IdempotencyTable', {
partitionKey: { name: 'eventId', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
timeToLiveAttribute: 'expiresAt',
removalPolicy: RemovalPolicy.RETAIN,
});
// Dead letter queue
const dlq = new sqs.Queue(this, 'OrderProcessingDLQ', {
retentionPeriod: Duration.days(14),
encryption: sqs.QueueEncryption.KMS_MANAGED,
});
// Main processing queue
const orderQueue = new sqs.Queue(this, 'OrderProcessingQueue', {
visibilityTimeout: Duration.seconds(300),
encryption: sqs.QueueEncryption.KMS_MANAGED,
deadLetterQueue: { queue: dlq, maxReceiveCount: 3 },
});
// Lambda processor
const orderProcessor = new lambda.Function(this, 'OrderProcessor', {
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda/order-processor'),
environment: { IDEMPOTENCY_TABLE: idempotencyTable.tableName },
timeout: Duration.seconds(30),
reservedConcurrentExecutions: 50,
});
orderProcessor.addEventSource(
new lambdaEventSources.SqsEventSource(orderQueue, {
batchSize: 10,
reportBatchItemFailures: true,
})
);
idempotencyTable.grantReadWriteData(orderProcessor);
// EventBridge custom bus + rule
const orderBus = new events.EventBus(this, 'OrderEventBus', {
eventBusName: 'orders',
});
new events.Rule(this, 'OrderCreatedRule', {
eventBus: orderBus,
eventPattern: {
source: ['com.ascendion.orders'],
detailType: ['OrderCreated', 'OrderUpdated'],
},
targets: [new targets.SqsQueue(orderQueue)],
});
}
}
Lambda Handler — Idempotent Processing
import { SQSEvent, SQSBatchResponse } from 'aws-lambda';
import { DynamoDBClient, PutItemCommand,
ConditionalCheckFailedException } from '@aws-sdk/client-dynamodb';
const ddb = new DynamoDBClient({});
export const handler = async (event: SQSEvent): Promise<SQSBatchResponse> => {
const failures: string[] = [];
for (const record of event.Records) {
const body = JSON.parse(record.body);
const eventId = body['id'] ?? record.messageId;
try {
// Idempotency check — conditional write fails if already processed
await ddb.send(new PutItemCommand({
TableName: process.env.IDEMPOTENCY_TABLE!,
Item: {
eventId: { S: eventId },
expiresAt: { N: String(Math.floor(Date.now() / 1000) + 86400) },
},
ConditionExpression: 'attribute_not_exists(eventId)',
}));
await processOrder(body);
} catch (err) {
if (err instanceof ConditionalCheckFailedException) {
continue; // Already processed — skip silently
}
failures.push(record.messageId);
}
}
return { batchItemFailures: failures.map(id => ({ itemIdentifier: id })) };
};
Decision Criteria
| Scenario | Recommended Service | Reason |
|---|---|---|
| One producer, one consumer | SQS directly | No routing needed, lowest latency |
| One producer, many consumers | SNS + SQS fan-out | Each consumer gets own queue with DLQ |
| Content-based routing | EventBridge | Rule-based routing on event content |
| Cross-account event delivery | EventBridge | Built-in cross-account bus targets |
| Ordered processing per entity | SQS FIFO with MessageGroupId | Guarantees order within a group |
| High-throughput, no ordering | SQS Standard | Highest throughput, lowest cost |
| Scheduled events | EventBridge Scheduler | Replaces cron, no infrastructure |
| Third-party SaaS events | EventBridge Partner Sources | Shopify, Stripe, GitHub built-in |
Cost Model
| Service | Pricing Unit | Typical monthly cost at 10M events |
|---|---|---|
| EventBridge | $1.00 per million events | ~$10 |
| SQS Standard | $0.40 per million requests | ~$8 |
| Lambda | $0.20 per million invocations + duration | ~$5–20 |
| DynamoDB (idempotency) | Pay-per-request | ~$2 |
Cost optimisation levers:
- Use SQS batching in Lambda (batchSize: 10) — reduces Lambda invocations by 10×
- Set SQS message retention to minimum needed — reduces storage cost
- Use Lambda Graviton2 (ARM) runtime — 20% cheaper than x86 at same performance
- Archive cold events to S3 + query with Athena instead of keeping in DynamoDB
Anti-Patterns to Avoid
Implementing a scheduler that queries a database every 30 seconds to detect new orders, creating N×M database load as services and polling frequency grow. Indistinguishable from a distributed monolith under load.
Publish an event to EventBridge when state changes. Zero polling, instant propagation, no database load from consumers.
Lambda consumer calls another service synchronously and waits for the response before acknowledging the SQS message. If the downstream service is slow or unavailable, the Lambda times out, the message returns to the queue, and the same overloaded service gets called again in an amplifying loop.
Publish a second event to EventBridge or SQS rather than calling downstream services directly. Each consumer does one thing and hands off via events.
Processing SQS messages without checking for duplicate delivery. SQS Standard guarantees at-least-once delivery — the same message can arrive twice. Without an idempotency check, orders are fulfilled twice, payments charged twice, audit records duplicated.
Write an idempotency record to DynamoDB with a conditional expression before processing. If the condition fails, the event was already handled — skip it silently.
Creating an SQS queue with a DLQ but setting no CloudWatch alarm on the DLQ depth. Failed events accumulate silently for 14 days then vanish. Production data loss with no alert.
Create a CloudWatch alarm on ApproximateNumberOfMessagesVisible on the DLQ. Alert at depth > 0. Treat every DLQ message as an incident.
Flowchart
References
- AWS — Amazon EventBridge User Guide. docs.aws.amazon.com/eventbridge
- AWS — Amazon SQS Developer Guide. docs.aws.amazon.com/sqs
- AWS — Lambda Event Source Mapping. docs.aws.amazon.com/lambda/latest/dg/invocation-eventsourcemapping
- AWS — Well-Architected Framework — Reliability Pillar. docs.aws.amazon.com/wellarchitected
- Richardson, Chris — Microservices Patterns. Manning, 2018. microservices.io/patterns
- AWS re:Invent — Building event-driven architectures on AWS. youtube.com/aws