CQRS + Event Sourcing
AWS architecture template · Command and Query Responsibility Segregation with DynamoDB Streams as the event bus. Commands append immutable events; a projector builds a denormalised read model.
CQRS + Event Sourcing is a free AWS architecture template built with Lambda, DynamoDB, IAM, API Gateway, Athena, CloudWatch and S3. Open it in TheArchitect to edit the diagram, estimate monthly cost, and export the CloudFormation or Terraform that deploys it.
AWS services in this template
- Lambda×3
- DynamoDB×2
- IAM×2
- API Gateway
- Athena
- CloudWatch
- S3
Deployment notes
Needs input
- Enable DynamoDB Streams on the event store table (NEW_IMAGE) before deploying projector Lambda.
- Create the read model GSI for the query access pattern.
Generated CloudFormation — 12 resources
| Logical ID | Resource type |
|---|---|
| CommandQueryAPIapigateway | AWS::ApiGateway::RestApi |
| CommandHandlerlambda | AWS::Lambda::Function |
| EventStoredynamodb | AWS::DynamoDB::Table |
| Projectorlambda | AWS::Lambda::Function |
| ReadModeldynamodb | AWS::DynamoDB::Table |
| QueryHandlerlambda | AWS::Lambda::Function |
| EventArchives3 | AWS::S3::Bucket |
| ReplayAuditathena | AWS::Athena::WorkGroup |
| CommandRoleiam | AWS::IAM::Role |
| ProjectorRoleiam | AWS::IAM::Role |
| ProjectionLagAlarmcloudwatch | AWS::CloudWatch::Alarm |
| ProjectorlambdaEventArchives3Permission | AWS::Lambda::Permission |
View the full CloudFormation template
{
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "Generated by TheArchitect — https://www.thecloudarchitect.dev",
"Metadata": {
"TheArchitect": {
"Edges": [
{
"From": "CommandHandlerlambda",
"To": "EventStoredynamodb",
"Label": "conditional append"
},
{
"From": "EventStoredynamodb",
"To": "Projectorlambda",
"Label": "DynamoDB Stream"
},
{
"From": "Projectorlambda",
"To": "ReadModeldynamodb",
"Label": "upsert projection"
},
{
"From": "Projectorlambda",
"To": "EventArchives3",
"Label": "archive event"
},
{
"From": "QueryHandlerlambda",
"To": "ReadModeldynamodb",
"Label": "read projection"
},
{
"From": "EventArchives3",
"To": "ReplayAuditathena",
"Label": "audit queries"
},
{
"From": "CommandRoleiam",
"To": "CommandHandlerlambda",
"Label": "execution role"
},
{
"From": "ProjectorRoleiam",
"To": "Projectorlambda",
"Label": "execution role"
},
{
"From": "Projectorlambda",
"To": "ProjectionLagAlarmcloudwatch",
"Label": "iterator age"
}
]
}
},
"Resources": {
"CommandQueryAPIapigateway": {
"Type": "AWS::ApiGateway::RestApi",
"Properties": {
"Name": "cqrs-api",
"Description": "Unified command and query REST API",
"EndpointConfiguration": {
"Types": [
"REGIONAL"
]
}
}
},
"CommandHandlerlambda": {
"Type": "AWS::Lambda::Function",
"DependsOn": [
"CommandRoleiam"
],
"Properties": {
"FunctionName": "cqrs-command-handler",
"Runtime": "nodejs20.x",
"Handler": "command.handler",
"MemorySize": 512,
"Timeout": 15,
"Description": "Validates command, appends immutable event to event store using conditional write on (aggregate_id, version) to prevent duplicate events.",
"Role": {
"Fn::GetAtt": [
"CommandRoleiam",
"Arn"
]
}
}
},
"EventStoredynamodb": {
"Type": "AWS::DynamoDB::Table",
"Properties": {
"TableName": "event-store",
"BillingMode": "PAY_PER_REQUEST",
"AttributeDefinitions": [
{
"AttributeName": "id",
"AttributeType": "S"
}
],
"KeySchema": [
{
"AttributeName": "id",
"KeyType": "HASH"
}
],
"PointInTimeRecoverySpecification": {
"PointInTimeRecoveryEnabled": true
}
}
},
"Projectorlambda": {
"Type": "AWS::Lambda::Function",
"DependsOn": [
"ProjectorRoleiam"
],
"Properties": {
"FunctionName": "cqrs-projector",
"Runtime": "nodejs20.x",
"Handler": "projector.handler",
"MemorySize": 512,
"Timeout": 60,
"Description": "Triggered by DynamoDB Stream (NEW_IMAGE). Rebuilds the read model projection in the read model table and archives the event to S3.",
"Role": {
"Fn::GetAtt": [
"ProjectorRoleiam",
"Arn"
]
}
}
},
"ReadModeldynamodb": {
"Type": "AWS::DynamoDB::Table",
"Properties": {
"TableName": "read-model",
"BillingMode": "PAY_PER_REQUEST",
"AttributeDefinitions": [
{
"AttributeName": "id",
"AttributeType": "S"
}
],
"KeySchema": [
{
"AttributeName": "id",
"KeyType": "HASH"
}
]
}
},
"QueryHandlerlambda": {
"Type": "AWS::Lambda::Function",
"Properties": {
"FunctionName": "cqrs-query-handler",
"Runtime": "nodejs20.x",
"Handler": "query.handler",
"MemorySize": 256,
"Timeout": 10,
"Description": "Reads exclusively from the denormalised read model. No access to the event store — enforces CQRS separation."
}
},
"EventArchives3": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BucketName": "cqrs-event-archive",
"VersioningConfiguration": {
"Status": "Enabled"
},
"PublicAccessBlockConfiguration": {
"BlockPublicAcls": true,
"BlockPublicPolicy": true,
"IgnorePublicAcls": true,
"RestrictPublicBuckets": true
},
"NotificationConfiguration": {
"LambdaConfigurations": [
{
"Event": "s3:ObjectCreated:*",
"Function": {
"Fn::GetAtt": [
"Projectorlambda",
"Arn"
]
}
}
]
}
},
"DependsOn": [
"ProjectorlambdaEventArchives3Permission"
]
},
"ReplayAuditathena": {
"Type": "AWS::Athena::WorkGroup",
"Properties": {
"Name": "cqrs-replay-audit",
"Description": "Replay and audit queries against the immutable event archive",
"State": "ENABLED",
"WorkGroupConfiguration": {
"PublishCloudWatchMetricsEnabled": false,
"BytesScannedCutoffPerQuery": 5368709120,
"ResultConfiguration": {
"OutputLocation": "s3://cqrs-event-archive/athena-results/"
}
}
}
},
"CommandRoleiam": {
"Type": "AWS::IAM::Role",
"Properties": {
"RoleName": "cqrs-command-role",
"Description": "Command handler Lambda role — event-store DynamoDB write only",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
},
"ManagedPolicyArns": [
"arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
]
}
},
"ProjectorRoleiam": {
"Type": "AWS::IAM::Role",
"Properties": {
"RoleName": "cqrs-projector-role",
"Description": "Projector Lambda role — DynamoDB stream read, read-model write, S3 archive write",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
},
"ManagedPolicyArns": [
"arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
]
}
},
"ProjectionLagAlarmcloudwatch": {
"Type": "AWS::CloudWatch::Alarm",
"Properties": {
"AlarmName": "cqrs-projection-lag",
"AlarmDescription": "DynamoDB Streams IteratorAge > 30 s — projector is falling behind commands",
"Namespace": "AWS/Lambda",
"MetricName": "IteratorAge",
"Statistic": "Maximum",
"Period": 60,
"EvaluationPeriods": 3,
"Threshold": 30000,
"ComparisonOperator": "GreaterThanThreshold",
"TreatMissingData": "notBreaching"
}
},
"ProjectorlambdaEventArchives3Permission": {
"Type": "AWS::Lambda::Permission",
"DependsOn": [
"Projectorlambda"
],
"Properties": {
"Action": "lambda:InvokeFunction",
"FunctionName": {
"Ref": "Projectorlambda"
},
"Principal": "s3.amazonaws.com",
"SourceAccount": {
"Ref": "AWS::AccountId"
},
"SourceArn": {
"Fn::Sub": "arn:${AWS::Partition}:s3:::cqrs-event-archive"
}
}
}
}
}Generated by TheArchitect from the diagram above. Review values such as names, CIDR ranges and instance sizes before deploying to your account.
Related templates
- ML Inference + Feature StoreServerless ML serving with a dual feature store: Redis for low-latency online features and S3 for offline training set assembly via Athena.
- Twitter/X Scale TestCloudFront behavior split, API Gateway VPC Link, public/internal ALBs, private app/data subnets, queues, media, and data stores
- IoT Fleet Telemetry PipelineKinesis stream ingests device telemetry with three parallel Lambda consumers: anomaly detection, device shadow updates, and cold-path S3 archival for Athena analytics.