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.