Serverless Patterns & Best Practices
Common Patterns
Simple Explanation
What it is
This lesson collects proven serverless design patterns so you do not have to reinvent them.
Why we need it
Patterns save time. They let you choose a known-good architecture instead of guessing.
Benefits
- Faster design decisions with proven templates.
- More reliable systems because patterns handle common failures.
- Easier communication across teams.
Tradeoffs
- Patterns are not one-size-fits-all and must be adapted.
- Overuse can lead to unnecessary complexity.
Real-world examples (architecture only)
- API pattern for CRUD apps.
- Event-driven pattern for background processing.
1. API Pattern
REST API backed by Lambda + DynamoDB:
Most common, suitable for CRUD operations.
2. Event-Driven Pattern
Events trigger workflows:
Decoupled, scalable.
3. Stream Processing Pattern
Process continuous data streams:
Real-time analytics.
4. Scheduled Pattern
Cron-like tasks:
No manual scheduling needed.
5. Queue Pattern
Handle async workloads:
Decouple producers from consumers.
Three-Tier Architecture
Each tier can scale independently.
Microservices Pattern
Split large app into small services:
Each service:
- Has own database
- Owned by small team
- Scales independently
- Can fail independently
Webhook Pattern
External systems trigger your Lambda:
Automation without polling.
Pub/Sub Pattern
One service publishes, many subscribe:
Loose coupling, easy to add new subscribers.
SAGA Pattern (Distributed Transactions)
Coordinate across services:
Use Step Functions to orchestrate.
Best Practices
1. Single Responsibility
One function, one job:
# ❌ Bad: Handles everything
def handler(event, context):
return send_email(handle_db(handle_auth(handle_request(event))))
# ✅ Good: Focused
def handler(event, context):
return process_order(event)
2. Environment Configuration
Don't hardcode settings:
Globals:
Function:
Environment:
Variables:
TABLE_NAME: !Ref ItemsTable
BUCKET_NAME: !Ref MyBucket
STAGE: !Ref Environment
3. Logging & Monitoring
Every function should log:
import json
from datetime import datetime
def log_start(context):
print(json.dumps({
"timestamp": datetime.utcnow().isoformat(),
"level": "INFO",
"function": context.function_name,
"requestId": context.aws_request_id,
"message": "Processing started",
}))
4. Idempotency
Handle duplicate invocations:
# Async events might be retried
invoked = cache.get(event.get("idempotencyKey"))
if invoked:
return invoked["result"]
result = process(event)
cache.set(event.get("idempotencyKey"), {"result": result})
return result
5. Error Handling
Catch and log errors:
def handler(event, context):
try:
return process_event(event)
except Exception as exc:
logger.error("Failed to process", extra={"error": str(exc), "event": event})
if getattr(exc, "retryable", False):
raise
return {"statusCode": 400, "error": str(exc)}
6. Security
Least privilege IAM:
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-bucket/uploads/*",
"Condition": {
"IpAddress": {
"aws:SourceIp": "10.0.0.0/8"
}
}
}
Only what's needed, only from where it's needed.
7. Cost Awareness
Track costs per function:
aws ce get-cost-and-usage \
--filter file://function-costs.json
# Identify expensive functions
# Optimize or deprecate
8. Version Control for Infrastructure
Store SAM templates in git:
Track changes, review PRs, maintain history.
Antipatterns
❌ Long-Running Functions
Lambda times out after 15 minutes:
# ❌ Bad: Process 10M items in Lambda
items = get_all()
for item in items:
process(item)
# ✅ Good: Use Step Functions or Batch
❌ Tight Coupling
# ❌ Bad: Calls other Lambda directly
result = lambda_client.invoke(FunctionName="OtherFunc", Payload=payload)
# ✅ Good: Use events/queues
sns_client.publish(TopicArn="arn:...", Message=json.dumps(event))
❌ State in Memory
# ❌ Bad: Relies on persistent state
request_count = 0
def handler(event, context):
global request_count
request_count += 1
# ✅ Good: Stateless
def handler_stateless(event, context):
request_count = ddb.get_item(Key={"id": "request-count"})
return request_count
❌ Hardcoded Secrets
# ❌ Bad: Secret in code
API_KEY = "secret-key-12345"
# ✅ Good: Use Secrets Manager
secret = secrets_manager.get_secret_value(SecretId="api-key")
api_key = secret.get("SecretString")
Architecture Decision Records (ADR)
Document why you chose each pattern:
# ADR-001: Use Queue-Based Processing
## Context
API surge during peak hours causes timeouts.
## Decision
Use SQS + Lambda for async processing.
## Rationale
- Decouples request from processing
- Smooths traffic spikes
- Easier to scale independently
## Consequences
- Added latency (users don't see results immediately)
- More infrastructure
- Higher operational complexity
## Status
Approved on 2026-02-08
Key Takeaway
Serverless isn't about avoiding architecture; it's about different trade-offs. Choose patterns that match your problem, not fashion.