Skip to main content

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:

API pattern

Most common, suitable for CRUD operations.

2. Event-Driven Pattern​

Events trigger workflows:

Event-driven pattern

Decoupled, scalable.

3. Stream Processing Pattern​

Process continuous data streams:

Stream processing pattern

Real-time analytics.

4. Scheduled Pattern​

Cron-like tasks:

Scheduled pattern

No manual scheduling needed.

5. Queue Pattern​

Handle async workloads:

Queue pattern

Decouple producers from consumers.

Three-Tier Architecture​

Three-tier architecture

Each tier can scale independently.

Microservices Pattern​

Split large app into small services:

Microservices pattern

Each service:

  • Has own database
  • Owned by small team
  • Scales independently
  • Can fail independently

Webhook Pattern​

External systems trigger your Lambda:

Webhook pattern

Automation without polling.

Pub/Sub Pattern​

One service publishes, many subscribe:

Pub/Sub fan-out

Loose coupling, easy to add new subscribers.

SAGA Pattern (Distributed Transactions)​

Coordinate across services:

Saga steps

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:

Repository structure

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.