Building Event-Driven Architecture with Amazon EventBridge
9 min read
Microservices promised us loosely coupled, independently deployable services. But then reality hit: service-to-service HTTP calls created tight coupling, cascading failures, and a distributed monolith. Sound familiar?
The solution isn't more REST APIs - it's event-driven architecture. And in the AWS ecosystem, Amazon EventBridge has emerged as the central nervous system for building truly decoupled, scalable event-driven applications.
In this comprehensive guide, we'll explore how to leverage EventBridge to build production-grade event-driven architectures that scale to millions of events while maintaining loose coupling and resilience.
Why EventBridge? The Event-Driven Evolution
Traditional request-response patterns create tight coupling:

With EventBridge, services become independent event producers and consumers:

Each service operates independently. Adding a new consumer? Just subscribe to the event - no changes to the producer.
EventBridge Core Concepts
Event Buses: The Foundation
EventBridge organizes events into event buses - logical channels that receive and route events. Three types exist:
1. Default Event Bus
Automatically created in every AWS account
Receives events from AWS services (S3, EC2, RDS, etc.)
No manual creation needed
2. Custom Event Bus
For your application's custom events
Isolate different environments or domains
Enable cross-account event routing
3. Partner Event Bus
Receives events from SaaS providers
Examples: Datadog, Auth0, PagerDuty
Automatically created when you set up a partner integration
Event Patterns: Smart Filtering
Event patterns define which events trigger your rules. They use JSON matching:
{
"source": ["orders.service"],
"detail-type": ["Order Placed"],
"detail": {
"amount": [{ "numeric": [">", 1000] }],
"region": ["us-east-1", "us-west-2"]
}
}
This pattern matches only orders over $1,000 in specific regions-precise, declarative filtering.
Targets: Where Events Go
Each rule can route to up to 5 targets simultaneously:
Lambda functions
Step Functions state machines
SQS queues
SNS topics
Kinesis streams
API destinations (HTTP endpoints)
CloudWatch log groups
Real-World Architecture: E-Commerce Order Processing
Let's build a production-ready order processing system:

Step 1: Define Your Event Schema
First, establish a consistent event structure:
// events/order-events.ts
export interface OrderPlacedEvent {
version: '1.0';
id: string;
source: 'orders.service';
'detail-type': 'Order Placed';
time: string; // ISO 8601
region: string;
detail: {
orderId: string;
customerId: string;
items: Array<{
productId: string;
quantity: number;
price: number;
}>;
totalAmount: number;
shippingAddress: {
street: string;
city: string;
state: string;
zipCode: string;
country: string;
};
metadata: {
source: 'web' | 'mobile' | 'api';
promotionCode?: string;
};
};
}
Step 2: Create Custom Event Bus
Using AWS CDK:
// lib/event-bus-stack.ts
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 lambda from 'aws-cdk-lib/aws-lambda';
export class OrderEventBusStack extends cdk.Stack {
public readonly eventBus: events.EventBus;
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Create custom event bus for orders domain
this.eventBus = new events.EventBus(this, 'OrderEventBus', {
eventBusName: 'orders-event-bus',
});
// Enable event archiving for replay capability
new events.Archive(this, 'OrderEventArchive', {
sourceEventBus: this.eventBus,
archiveName: 'orders-archive',
description: 'Archive all order events for 90 days',
eventPattern: {
source: ['orders.service'],
},
retention: cdk.Duration.days(90),
});
// Output the event bus ARN for cross-stack references
new cdk.CfnOutput(this, 'EventBusArn', {
value: this.eventBus.eventBusArn,
exportName: 'OrderEventBusArn',
});
}
}
Using AWS SAM:
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
OrderEventBus:
Type: AWS::Events::EventBus
Properties:
Name: orders-event-bus
OrderEventArchive:
Type: AWS::Events::Archive
Properties:
ArchiveName: orders-archive
SourceArn: !GetAtt OrderEventBus.Arn
Description: Archive all order events for 90 days
RetentionDays: 90
EventPattern:
source:
- orders.service
Outputs:
EventBusArn:
Value: !GetAtt OrderEventBus.Arn
Export:
Name: OrderEventBusArn
Step 3: Publish Events from Order Service
# order_service/handler.py
import json
import boto3
import os
from datetime import datetime
from uuid import uuid4
from aws_lambda_powertools import Logger, Tracer
from aws_lambda_powertools.utilities.typing import LambdaContext
logger = Logger()
tracer = Tracer()
eventbridge = boto3.client('events')
EVENT_BUS_NAME = os.environ['EVENT_BUS_NAME']
@tracer.capture_lambda_handler
def create_order(event: dict, context: LambdaContext) -> dict:
"""
Create order and publish event to EventBridge
"""
try:
# Parse request body
body = json.loads(event['body'])
# Generate order ID
order_id = f"ORD-{uuid4().hex[:8].upper()}"
# Validate and process order
order = {
'orderId': order_id,
'customerId': body['customerId'],
'items': body['items'],
'totalAmount': sum(item['price'] * item['quantity'] for item in body['items']),
'shippingAddress': body['shippingAddress'],
'metadata': {
'source': body.get('source', 'web'),
'promotionCode': body.get('promotionCode')
}
}
# Persist to DynamoDB (omitted for brevity)
# save_order_to_dynamodb(order)
# Publish event to EventBridge
publish_order_placed_event(order)
logger.info("Order created successfully", extra={
"order_id": order_id,
"customer_id": order['customerId'],
"total_amount": order['totalAmount']
})
return {
'statusCode': 201,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({
'orderId': order_id,
'message': 'Order created successfully'
})
}
except Exception as e:
logger.exception("Failed to create order")
return {
'statusCode': 500,
'body': json.dumps({'error': 'Internal server error'})
}
def publish_order_placed_event(order: dict) -> None:
"""
Publish OrderPlaced event to EventBridge
"""
event_detail = {
'version': '1.0',
'id': str(uuid4()),
'source': 'orders.service',
'detail-type': 'Order Placed',
'time': datetime.utcnow().isoformat() + 'Z',
'region': os.environ['AWS_REGION'],
'detail': order
}
response = eventbridge.put_events(
Entries=[
{
'Source': 'orders.service',
'DetailType': 'Order Placed',
'Detail': json.dumps(order),
'EventBusName': EVENT_BUS_NAME
}
]
)
# Check for failures
if response['FailedEntryCount'] > 0:
logger.error("Failed to publish event", extra={
"failed_entries": response['Entries']
})
raise Exception("Failed to publish event to EventBridge")
logger.info("Event published successfully", extra={
"event_id": response['Entries'][0]['EventId']
})
Step 4: Create Event Rules and Targets
// lib/order-rules-stack.ts
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 lambda from 'aws-cdk-lib/aws-lambda';
import * as sqs from 'aws-cdk-lib/aws-sqs';
export class OrderRulesStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Import the event bus
const eventBus = events.EventBus.fromEventBusArn(
this,
'OrderEventBus',
cdk.Fn.importValue('OrderEventBusArn')
);
// Lambda functions (create these separately)
const inventoryFunction = lambda.Function.fromFunctionName(
this,
'InventoryFunction',
'inventory-service'
);
const emailFunction = lambda.Function.fromFunctionName(
this,
'EmailFunction',
'email-service'
);
// Dead letter queue for failed events
const dlq = new sqs.Queue(this, 'OrderEventsDLQ', {
queueName: 'order-events-dlq',
retentionPeriod: cdk.Duration.days(14),
});
// Rule 1: All order events to inventory service
new events.Rule(this, 'OrderToInventoryRule', {
eventBus: eventBus,
ruleName: 'order-to-inventory',
description: 'Route all order events to inventory service',
eventPattern: {
source: ['orders.service'],
detailType: ['Order Placed'],
},
targets: [
new targets.LambdaFunction(inventoryFunction, {
deadLetterQueue: dlq,
maxEventAge: cdk.Duration.hours(2),
retryAttempts: 2,
}),
],
});
// Rule 2: High-value orders (>$1000) to email service
new events.Rule(this, 'HighValueOrderEmailRule', {
eventBus: eventBus,
ruleName: 'high-value-order-email',
description: 'Send email for high-value orders',
eventPattern: {
source: ['orders.service'],
detailType: ['Order Placed'],
detail: {
totalAmount: [{ numeric: ['>', 1000] }],
},
},
targets: [
new targets.LambdaFunction(emailFunction, {
deadLetterQueue: dlq,
maxEventAge: cdk.Duration.hours(1),
retryAttempts: 3,
}),
],
});
// Rule 3: Orders with promo codes to analytics
const analyticsQueue = new sqs.Queue(this, 'AnalyticsQueue', {
queueName: 'analytics-queue',
});
new events.Rule(this, 'PromoCodeAnalyticsRule', {
eventBus: eventBus,
ruleName: 'promo-code-analytics',
description: 'Track orders with promotion codes',
eventPattern: {
source: ['orders.service'],
detailType: ['Order Placed'],
detail: {
metadata: {
promotionCode: [{ exists: true }],
},
},
},
targets: [
new targets.SqsQueue(analyticsQueue),
],
});
}
}
Step 5: Consume Events in Downstream Services
# inventory_service/handler.py
import json
import boto3
from aws_lambda_powertools import Logger, Tracer
from aws_lambda_powertools.utilities.typing import LambdaContext
from aws_lambda_powertools.utilities.data_classes import EventBridgeEvent
logger = Logger()
tracer = Tracer()
dynamodb = boto3.resource('dynamodb')
inventory_table = dynamodb.Table('inventory')
@tracer.capture_lambda_handler
def process_order(event: dict, context: LambdaContext) -> dict:
"""
Process order event from EventBridge
Reduce inventory quantities for ordered items
"""
# Parse EventBridge event
eb_event = EventBridgeEvent(event)
logger.info("Processing order event", extra={
"event_id": eb_event.id,
"source": eb_event.source,
"detail_type": eb_event.detail_type
})
order = eb_event.detail
order_id = order['orderId']
items = order['items']
try:
# Reserve inventory for each item
for item in items:
product_id = item['productId']
quantity = item['quantity']
# Update inventory with conditional check
response = inventory_table.update_item(
Key={'productId': product_id},
UpdateExpression='SET availableQuantity = availableQuantity - :qty, reservedQuantity = reservedQuantity + :qty',
ConditionExpression='availableQuantity >= :qty',
ExpressionAttributeValues={
':qty': quantity
},
ReturnValues='UPDATED_NEW'
)
logger.info("Inventory reserved", extra={
"product_id": product_id,
"quantity": quantity,
"new_available": response['Attributes']['availableQuantity']
})
# Publish inventory reserved event
publish_inventory_event(order_id, 'reserved', items)
return {
'statusCode': 200,
'body': json.dumps({'message': 'Inventory reserved successfully'})
}
except dynamodb.meta.client.exceptions.ConditionalCheckFailedException:
logger.error("Insufficient inventory", extra={"order_id": order_id})
# Publish out-of-stock event
publish_inventory_event(order_id, 'out-of-stock', items)
return {
'statusCode': 409,
'body': json.dumps({'error': 'Insufficient inventory'})
}
except Exception as e:
logger.exception("Failed to process order")
raise
def publish_inventory_event(order_id: str, status: str, items: list) -> None:
"""
Publish inventory status event back to EventBridge
"""
eventbridge = boto3.client('events')
eventbridge.put_events(
Entries=[
{
'Source': 'inventory.service',
'DetailType': f'Inventory {status.title()}',
'Detail': json.dumps({
'orderId': order_id,
'status': status,
'items': items
}),
'EventBusName': 'orders-event-bus'
}
]
)
Advanced Patterns
Pattern 1: Event Transformation with Input Transformer
Transform event structure before sending to target:
new events.Rule(this, 'TransformRule', {
eventBus: eventBus,
eventPattern: {
source: ['orders.service'],
detailType: ['Order Placed'],
},
targets: [
new targets.LambdaFunction(processingFunction, {
event: events.RuleTargetInput.fromObject({
orderId: events.EventField.fromPath('$.detail.orderId'),
totalAmount: events.EventField.fromPath('$.detail.totalAmount'),
timestamp: events.EventField.fromPath('$.time'),
// Add custom fields
processedBy: 'event-transformer',
priority: 'high',
}),
}),
],
});
Pattern 2: Cross-Account Event Routing
Share events across AWS accounts:
// In Account A (producer)
const accountBEventBus = 'arn:aws:events:us-east-1:222222222222:event-bus/shared-events';
new events.Rule(this, 'CrossAccountRule', {
eventBus: eventBus,
eventPattern: {
source: ['orders.service'],
},
targets: [
new targets.EventBus(
events.EventBus.fromEventBusArn(this, 'AccountBBus', accountBEventBus)
),
],
});
// In Account B (consumer) - add permissions
const eventBusPolicy = new events.CfnEventBusPolicy(this, 'CrossAccountPolicy', {
statementId: 'AllowAccountA',
eventBusName: eventBus.eventBusName,
statement: {
Effect: 'Allow',
Principal: { AWS: 'arn:aws:iam::111111111111:root' },
Action: 'events:PutEvents',
Resource: eventBus.eventBusArn,
},
});
Pattern 3: Event Replay for Testing
EventBridge archives enable event replay:
# Start replay from archive
aws events start-replay \
--replay-name order-replay-2025-05 \
--event-source-arn arn:aws:events:us-east-1:123456789012:event-bus/orders-event-bus \
--event-start-time '2025-05-01T00:00:00Z' \
--event-end-time '2025-05-31T23:59:59Z' \
--destination '{
"Arn": "arn:aws:events:us-east-1:123456789012:event-bus/test-event-bus",
"FilterArns": ["arn:aws:events:us-east-1:123456789012:rule/test-rule"]
}'
Monitoring and Debugging
CloudWatch Metrics
Track these key EventBridge metrics:
// Create CloudWatch dashboard
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
const dashboard = new cloudwatch.Dashboard(this, 'EventBridgeDashboard', {
dashboardName: 'order-events-dashboard',
});
dashboard.addWidgets(
new cloudwatch.GraphWidget({
title: 'Event Ingestion Rate',
left: [
new cloudwatch.Metric({
namespace: 'AWS/Events',
metricName: 'Invocations',
dimensionsMap: {
RuleName: 'order-to-inventory',
},
statistic: 'Sum',
period: cdk.Duration.minutes(5),
}),
],
}),
new cloudwatch.GraphWidget({
title: 'Failed Invocations',
left: [
new cloudwatch.Metric({
namespace: 'AWS/Events',
metricName: 'FailedInvocations',
dimensionsMap: {
RuleName: 'order-to-inventory',
},
statistic: 'Sum',
period: cdk.Duration.minutes(5),
}),
],
})
);
Dead Letter Queues
Always configure DLQs for fault tolerance:
# Monitor DLQ for failed events
import boto3
from aws_lambda_powertools import Logger
logger = Logger()
sqs = boto3.client('sqs')
def process_dlq_messages(event, context):
"""
Process failed events from DLQ
Alert on-call team and attempt recovery
"""
for record in event['Records']:
message_body = json.loads(record['body'])
logger.error("Event processing failed", extra={
"original_event": message_body,
"failure_reason": message_body.get('ErrorMessage'),
"retry_count": int(record.get('ApproximateReceiveCount', 0))
})
# Alert monitoring system
send_alert_to_pagerduty(message_body)
# Attempt manual recovery or store for investigation
store_failed_event_for_analysis(message_body)
Best Practices
Use Schema Registry: Define and version your event schemas
Implement Idempotency: Consumers should handle duplicate events gracefully
Set Appropriate Retry Policies: Configure maxEventAge and retryAttempts
Monitor Dead Letter Queues: Set up alerts for DLQ depth
Use Event Archives: Enable archiving for critical event types
Apply Least Privilege: IAM policies should grant minimal necessary permissions
Test Event Patterns: Validate patterns in EventBridge console before deploying
Version Your Events: Include version field in event detail for schema evolution
Cost Optimization
EventBridge pricing:
$1.00 per million events published to custom event bus
$0.00 per million events from AWS services to default bus
Archives and replays have storage costs ($0.10/GB/month)
Optimize costs by:
Using event filtering to reduce unnecessary Lambda invocations
Batching events when possible
Leveraging SQS queues as targets for buffering
Conclusion
Amazon EventBridge transforms how we build distributed systems on AWS. By embracing event-driven architecture, you achieve:
Loose coupling: Services evolve independently
Scalability: Handle millions of events per second
Resilience: Built-in retry, DLQs, and event archiving
Extensibility: Add new consumers without touching producers
Start with a single domain (like orders), establish event schemas, and expand from there. Your future self-debugging a distributed system at 2 AM - will thank you for the decoupling.
Building event-driven architectures? Share your EventBridge patterns and challenges in the comments!