Serverless & Event-Driven Architecture Patterns¶
Skill metadata
Name serverless-event-driven-architecture · Level senior · Tags serverless lambda eventbridge sqs step-functions platform-engineering
"Serverless and event-driven architecture: Lambda concurrency, EventBridge routing, SQS standard and FIFO queues with MessageGroupId ordering, Step Functions orchestration, dead-letter queues with redrive, idempotency, and cold-start mitigation. Use when designing event-driven flows, choosing FIFO versus standard for per-customer ordering at high throughput, or when events are lost, duplicated or throttled."
Source: skills/platform-engineering/serverless-event-driven-architecture/SKILL.md
When to Use This Skill¶
Triggers — load this skill when:
- An event-driven flow needs routing, ordering, or orchestration designed
- Failures must be captured with DLQs, retries, and idempotent handlers
- Cold starts, concurrency limits, or throttling are causing problems
Route elsewhere when:
- Long-running containerized services ->
cloud-native-microservices-patterns - Per-invocation cost modelling ->
finops-framework-inform-optimize-operate - Tracing across async hops ->
prometheus-grafana-otel-tracing
1. EventBridge Rule with Dead-Letter Queue (Terraform)¶
resource "aws_cloudwatch_event_rule" "order_placed" {
name = "order-placed-rule"
description = "Routes order.created events to invoice processing lambda"
event_pattern = jsonencode({
"source" = ["ecommerce.orders"],
"detail-type" = ["OrderCreated"]
})
}
resource "aws_cloudwatch_event_target" "lambda_target" {
rule = aws_cloudwatch_event_rule.order_placed.name
target_id = "InvoiceProcessingLambda"
arn = aws_lambda_function.invoice_lambda.arn
dead_letter_config {
arn = aws_sqs_queue.eventbridge_dlq.arn
}
retry_policy {
maximum_event_age_in_seconds = 3600
maximum_retry_attempts = 5
}
}
2. Best Practices & Anti-Patterns¶
- Do: Always configure Dead Letter Queues (DLQ) with alarms on every asynchronous invocation to prevent event loss.
- Do: Make event consumers idempotent using unique idempotency keys stored in DynamoDB/Redis.
- Don't: Never use synchronous Lambda-to-Lambda calls for business workflows; orchestrate with AWS Step Functions or EventBridge.
3. Delivery Guarantees: DLQ Redrive, Idempotency & FIFO Ordering¶
Nothing is lost, so nothing may be silently dropped. Every asynchronous hop needs its own failure destination — an EventBridge rule, a Lambda invocation, and an SQS consumer each fail differently:
resource "aws_sqs_queue" "orders_dlq" {
name = "orders-dlq"
message_retention_seconds = 1209600 # 14 days: room to fix and redrive
}
resource "aws_sqs_queue" "orders" {
name = "orders"
visibility_timeout_seconds = 180 # >= 6x the Lambda timeout
redrive_policy = jsonencode({
deadLetterTargetArn = aws_sqs_queue.orders_dlq.arn
maxReceiveCount = 5 # then park it, do not retry forever
})
}
resource "aws_sqs_queue_redrive_allow_policy" "dlq" {
queue_url = aws_sqs_queue.orders_dlq.id
redrive_allow_policy = jsonencode({
redrivePermission = "byQueue"
sourceQueueArns = [aws_sqs_queue.orders.arn] # enables DLQ redrive back to source
})
}
Redrive is an operational procedure, not a button: fix the defect, replay a single message, verify, then redrive the batch with a bounded rate.
At-least-once delivery means duplicates are normal. Every handler must be idempotent, keyed on a business identifier rather than the message ID (a retry after a partial write arrives with a new one):
def handler(event, _ctx):
for record in event["Records"]:
order = json.loads(record["body"])
try:
table.put_item(
Item={"pk": f"ORDER#{order['order_id']}", "state": "PROCESSED"},
ConditionExpression="attribute_not_exists(pk)", # dedupe = the write itself
)
except ClientError as e:
if e.response["Error"]["Code"] == "ConditionalCheckFailedException":
continue # already processed
raise
Return batchItemFailures for partial batch failure so one bad message does not replay the
whole batch.
Ordering: FIFO buys per-group order, not global order. MessageGroupId is the ordering and
concurrency unit — pick the entity whose order matters (customer_id, account_id), never a
constant:
| Standard | FIFO | |
|---|---|---|
| Order | Best-effort | Strict per MessageGroupId |
| Throughput | Effectively unlimited | 300 msg/s per group (3,000 batched; high-throughput mode raises the ceiling) |
| Duplicates | Possible | Deduplicated within a 5-minute window |
| Parallelism | Unbounded | One in-flight batch per group |
A single group serialises the whole queue; order_id as the group gives ordering nobody needed
at maximum cost. Group by the entity, keep handlers idempotent anyway, and use
MessageDeduplicationId for the 5-minute exactly-once window.