On This Page
1Overview2Architecture Overview
3AWS Service Topology4Implementation Guide
5Decision Criteria6Cost Model
7Anti-Patterns to Avoid8References

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

⚠ 1. Polling Instead of Event-Driven

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.

Hover to see the fix ↻
↺ Correct Approach

Publish an event to EventBridge when state changes. Zero polling, instant propagation, no database load from consumers.

⚠ 2. Synchronous Call Inside Lambda Consumer

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.

Hover to see the fix ↻
↺ Correct Approach

Publish a second event to EventBridge or SQS rather than calling downstream services directly. Each consumer does one thing and hands off via events.

⚠ 3. No Idempotency Key

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.

Hover to see the fix ↻
↺ Correct Approach

Write an idempotency record to DynamoDB with a conditional expression before processing. If the condition fails, the event was already handled — skip it silently.

⚠ 4. Ignoring the Dead Letter Queue

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.

Hover to see the fix ↻
↺ Correct Approach

Create a CloudWatch alarm on ApproximateNumberOfMessagesVisible on the DLQ. Alert at depth > 0. Treat every DLQ message as an incident.

Flowchart

%%{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 in Producer Service]) START --> EB[Amazon EventBridge\nCustom event bus\nSchema registry validation] EB --> ROUTE{Rule Matches\nEvent Pattern?} ROUTE -->|No match| ARCHIVE[EventBridge Archive\nReplay capability\nCross-account delivery] ROUTE -->|Single consumer| SQS[Amazon SQS\nReliable delivery\nVisibility timeout] ROUTE -->|Fan-out| SNS[Amazon SNS Topic\nMultiple SQS subscribers\nMessage filtering] SNS --> SQS SQS --> LAMBDA{Lambda Processes\nBatch of 10 messages} LAMBDA -->|Duplicate event| SKIP[Skip — idempotency\ncheck already processed] LAMBDA -->|New event| PROCESS[Process Business Logic\nWrite state to DynamoDB\nPublish downstream events] LAMBDA -->|Processing failed| DLQ[Dead Letter Queue\nCloudWatch alarm fires\nManual replay via console] PROCESS --> DONE([State Updated — Event Chain Complete]) style START fill:#4f8ef7,color:#fff style DONE fill:#10b981,color:#fff style DLQ fill:#fef3c7 style SKIP fill:#fef3c7 style ARCHIVE fill:#e0f2fe

References

  1. AWS — Amazon EventBridge User Guide. docs.aws.amazon.com/eventbridge
  2. AWS — Amazon SQS Developer Guide. docs.aws.amazon.com/sqs
  3. AWS — Lambda Event Source Mapping. docs.aws.amazon.com/lambda/latest/dg/invocation-eventsourcemapping
  4. AWS — Well-Architected Framework — Reliability Pillar. docs.aws.amazon.com/wellarchitected
  5. Richardson, Chris — Microservices Patterns. Manning, 2018. microservices.io/patterns
  6. AWS re:Invent — Building event-driven architectures on AWS. youtube.com/aws
Ascendion Engineering Knowledge Base ← Cloud