Serverless Architecture Cheat Sheet
Design patterns, tradeoffs, and best practices for building event-driven serverless applications across cloud providers.
Fan-Out Pattern (AWS SAM/CloudFormation)
One event triggers multiple parallel downstream functions via SNS.
Resources: OrderTopic: Type: AWS::SNS::Topic NotifyFn: Type: AWS::Serverless::Function Properties: Handler: notify.handler Events: SNS: Type: SNS Properties: Topic: !Ref OrderTopic InventoryFn: Type: AWS::Serverless::Function Properties: Handler: inventory.handler Events: SNS: Type: SNS Properties: Topic: !Ref OrderTopic
Idempotent Function Handler
Guard against duplicate event delivery, common in serverless.
processed_ids = set() # use durable store (e.g. DynamoDB) in productiondef handler(event, context): request_id = event['requestId'] if request_id in processed_ids: return {'statusCode': 200, 'body': 'already processed'} # ... do the actual work ... processed_ids.add(request_id) return {'statusCode': 200, 'body': 'processed'}
Core Patterns
Key core patterns to know.
- Function-as-a-Service (FaaS)- Short-lived, stateless functions triggered by events (Lambda, Cloud Functions, Azure Functions)
- Event-Driven Architecture- Services communicate asynchronously via events rather than direct calls
- Backend for Frontend (BFF)- API Gateway + Lambda tailored per client type (web, mobile)
- Saga Pattern- Chain of local transactions with compensating actions for distributed rollback
- Fan-out/Fan-in- Distribute work to many parallel functions, then aggregate results
Tradeoffs & Pitfalls
Key tradeoffs & pitfalls to know.
- Cold Starts- Latency spike when a new execution environment initializes
- Vendor Lock-in- Provider-specific triggers/APIs make migration harder
- Statelessness- Functions must externalize state (DB, cache) since instances aren't persistent
- Distributed Tracing- Essential for debugging across many short-lived, independently scaled functions
- Execution Limits- Max duration, memory, and payload size constrain what a single function can do
Provisioned Concurrency (AWS Lambda)
Pre-warm execution environments to eliminate cold starts for latency-sensitive functions.
Resources: CheckoutFn: Type: AWS::Serverless::Function Properties: Handler: checkout.handler Runtime: python3.12 AutoPublishAlias: live ProvisionedConcurrencyConfig: ProvisionedConcurrentExecutions: 5 CheckoutFnScalingTarget: Type: AWS::ApplicationAutoScaling::ScalableTarget Properties: MinCapacity: 5 MaxCapacity: 50 ResourceId: !Sub "function:${CheckoutFn}:live" ScalableDimension: lambda:function:ProvisionedConcurrency ServiceNamespace: lambda RoleARN: !GetAtt AutoScalingRole.Arn
Step Functions Orchestration (Saga)
Coordinate multi-step Lambda workflows with built-in retries and compensating transactions.
{ "Comment": "Order saga with compensation", "StartAt": "ReserveInventory", "States": { "ReserveInventory": { "Type": "Task", "Resource": "arn:aws:lambda:us-east-1:123:function:reserveInventory", "Retry": [{ "ErrorEquals": ["States.TaskFailed"], "MaxAttempts": 3, "BackoffRate": 2.0 }], "Catch": [{ "ErrorEquals": ["States.ALL"], "Next": "ReleaseInventory" }], "Next": "ChargePayment" }, "ChargePayment": { "Type": "Task", "Resource": "arn:aws:lambda:us-east-1:123:function:chargePayment", "Catch": [{ "ErrorEquals": ["States.ALL"], "Next": "ReleaseInventory" }], "End": true }, "ReleaseInventory": { "Type": "Task", "Resource": "arn:aws:lambda:us-east-1:123:function:releaseInventory", "End": true } }}
Shared Lambda Layer for Connection Reuse
Reuse DB connections across warm invocations by initializing outside the handler.
import boto3import pymysqlimport os# Runs once per cold start, reused across warm invocations_conn = Nonedef get_connection(): global _conn if _conn is None or not _conn.open: secrets = boto3.client('secretsmanager') creds = secrets.get_secret_value(SecretId=os.environ['DB_SECRET_ARN']) _conn = pymysql.connect( host=os.environ['DB_HOST'], user='app', password=creds['SecretString'], connect_timeout=2, read_timeout=3 ) return _conndef handler(event, context): conn = get_connection() with conn.cursor() as cur: cur.execute("SELECT 1") return {'statusCode': 200, 'body': str(cur.fetchone())}
Serverless Observability Signals
What to actually instrument in a FaaS system beyond basic logs.
- Cold Start Duration- Separate INIT phase duration from invocation duration in traces to isolate startup cost
- Concurrent Executions- Track against account/region concurrency limits to catch throttling before it happens
- Iterator Age (stream triggers)- For Kinesis/DynamoDB Streams, rising iterator age means consumers are falling behind
- Dead-Letter Queue Depth- Non-zero DLQ depth signals silently failing async invocations that need investigation
- X-Ray / OpenTelemetry Segments- Distributed tracing across function boundaries is the only way to debug fan-out latency
- Duration p99 vs p50- A wide gap usually indicates cold starts or downstream dependency contention, not code inefficiency
Advanced Event Source Semantics
Delivery guarantees and ordering behavior that differ by trigger type.
- SQS Standard- At-least-once, best-effort ordering; visibility timeout must exceed function timeout to avoid duplicate processing
- SQS FIFO- Exactly-once processing with per-message-group ordering, at reduced throughput
- Kinesis / DynamoDB Streams- Strictly ordered per shard/partition; a poison-pill record blocks the shard until it succeeds or expires
- EventBridge- No ordering guarantee across rules; use archive & replay for reprocessing after a bug fix
- S3 Event Notifications- Can deliver duplicates and, rarely, out of order — never assume single delivery
Design every serverless handler to be idempotent from day one — most event sources (SQS, SNS, EventBridge, Pub/Sub) guarantee at-least-once delivery, meaning duplicate invocations are a certainty, not an edge case.