Serverless Architecture Patterns Explained -- Complete Guide
In this tutorial, you'll learn about Serverless Architecture Patterns Explained. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Serverless architecture patterns let you build scalable applications without managing servers. Event-driven functions, API backends, fan-out processing, and Event Sourcing are common patterns that take advantage of auto-scaling, pay-per-execution compute.
What You'll Learn
By the end of this tutorial, you will understand the main serverless architecture patterns, when to apply each one, and how to implement them with AWS Lambda, Azure Functions, and GCP Cloud Functions.
Why Serverless Architecture Matters
Serverless eliminates infrastructure management, scales from zero to thousands of concurrent executions, and charges only for actual usage.
Serverless Architecture Learning Path
flowchart LR
A[Cloud Compute] --> B[Serverless Architectures]
B --> C{You Are Here}
C --> D[Event-Driven]
C --> E[API Backend]
C --> F[Fan-Out]
C --> G[Event Sourcing]
D --> H[S3 -> Lambda]
D --> I[SQS -> Lambda]
E --> J[API Gateway -> Lambda]
E --> K[Lambda -> DynamoDB]
F --> L[SNS -> Multiple Lambdas]
F --> M[SQS -> Lambda Scaling]
G --> N[Event Store]
G --> O[Stream Processing]
Pattern 1: Event-Driven Architecture
Functions react to events from cloud services: S3 object creation, DynamoDB stream changes, SQS messages, or scheduled cron jobs.
# event-driven.tf
# Terraform: S3 event notification to Lambda
resource "aws_s3_bucket_notification" "uploads" {
bucket = aws_s3_bucket.uploads.id
lambda_function {
lambda_function_arn = aws_lambda_function.processor.arn
events = ["s3:ObjectCreated:*"]
filter_prefix = "incoming/"
filter_suffix = ".csv"
}
}
resource "aws_lambda_permission" "allow_s3" {
statement_id = "AllowExecutionFromS3"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.processor.function_name
principal = "s3.amazonaws.com"
source_arn = aws_s3_bucket.uploads.arn
}
resource "aws_lambda_function" "processor" {
filename = "processor.zip"
function_name = "dodatech-file-processor"
role = aws_iam_role.lambda_exec.arn
handler = "index.handler"
runtime = "python3.12"
memory_size = 512
timeout = 30
environment {
variables = {
DESTINATION_BUCKET = aws_s3_bucket.processed.id
LOG_LEVEL = "INFO"
}
}
}
resource "aws_lambda_function" "scheduled_cleanup" {
filename = "cleanup.zip"
function_name = "dodatech-scheduled-cleanup"
role = aws_iam_role.lambda_exec.arn
handler = "index.handler"
runtime = "python3.12"
memory_size = 128
timeout = 300
# Schedule: run daily at 3 AM
reserved_concurrent_executions = 1
}
resource "aws_cloudwatch_event_rule" "daily_cleanup" {
name = "dodatech-daily-cleanup"
schedule_expression = "cron(0 3 * * ? *)"
}
resource "aws_cloudwatch_event_target" "lambda_cleanup" {
rule = aws_cloudwatch_event_rule.daily_cleanup.name
arn = aws_lambda_function.scheduled_cleanup.arn
}
Pattern 2: Serverless API Backend
API Gateway + Lambda + DynamoDB forms the classic serverless API pattern. Each HTTP method maps to a Lambda function.
# serverless-api.yaml
# AWS SAM template for serverless API
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Globals:
Function:
Runtime: python3.12
Timeout: 10
MemorySize: 256
Tracing: Active
Resources:
UsersApi:
Type: AWS::Serverless::Api
Properties:
StageName: prod
Auth:
DefaultAuthorizer: AWS_IAM
MethodSettings:
- LoggingLevel: INFO
ResourcePath: "/*"
HttpMethod: "*"
CreateUserFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/
Handler: users.create_user
Events:
CreateUser:
Type: Api
Properties:
RestApiId: !Ref UsersApi
Path: /users
Method: POST
GetUserFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/
Handler: users.get_user
Events:
GetUser:
Type: Api
Properties:
RestApiId: !Ref UsersApi
Path: /users/{userId}
Method: GET
UsersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: dodatech-users
AttributeDefinitions:
- AttributeName: userId
AttributeType: S
KeySchema:
- AttributeName: userId
KeyType: HASH
BillingMode: PAY_PER_REQUEST
# src/users.py
# API handlers for user management
import json
import os
import uuid
import boto3
from datetime import datetime
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(os.environ.get('TABLE_NAME', 'dodatech-users'))
def create_user(event, context):
"""POST /users"""
body = json.loads(event.get('body', '{}'))
user = {
'userId': str(uuid.uuid4()),
'name': body.get('name'),
'email': body.get('email'),
'createdAt': datetime.now().isoformat(),
'active': True,
}
table.put_item(Item=user)
return {
'statusCode': 201,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps(user),
}
def get_user(event, context):
"""GET /users/{userId}"""
user_id = event['pathParameters']['userId']
response = table.get_item(Key={'userId': user_id})
user = response.get('Item')
if not user:
return {
'statusCode': 404,
'body': json.dumps({'error': 'User not found'}),
}
return {
'statusCode': 200,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps(user),
}
def list_users(event, context):
"""GET /users"""
response = table.scan()
users = response.get('Items', [])
return {
'statusCode': 200,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({'users': users, 'count': len(users)}),
}
Pattern 3: Fan-Out Processing
Fan-out distributes a single event to multiple downstream processors in parallel. SNS + SQS is the most common implementation.
# Fan-out architecture with SNS and SQS
# Create SNS topic
aws sns create-topic --name dodatech-events
# Create SQS queues for each consumer
aws sqs create-queue --queue-name dodatech-email-queue
aws sqs create-queue --queue-name dodatech-sms-queue
aws sqs create-queue --queue-name dodatech-analytics-queue
aws sqs create-queue --queue-name dodatech-audit-queue
# Subscribe each queue to the SNS topic
aws sns subscribe \
--topic-arn arn:aws:sns:us-east-1:123456789012:dodatech-events \
--protocol sqs \
--notification-endpoint arn:aws:sqs:us-east-1:123456789012:dodatech-email-queue
aws sns subscribe \
--topic-arn arn:aws:sns:us-east-1:123456789012:dodatech-events \
--protocol sqs \
--notification-endpoint arn:aws:sqs:us-east-1:123456789012:dodatech-analytics-queue
# Each Lambda function reads from its own queue
aws lambda create-event-source-mapping \
--function-name dodatech-email-sender \
--event-source-arn arn:aws:sqs:us-east-1:123456789012:dodatech-email-queue \
--batch-size 10
aws lambda create-event-source-mapping \
--function-name dodatech-analytics-tracker \
--event-source-arn arn:aws:sqs:us-east-1:123456789012:dodatech-analytics-queue \
--batch-size 10
Pattern 4: Event Sourcing with DynamoDB Streams
Event Sourcing stores state changes as an ordered sequence of events. DynamoDB Streams captures item-level changes and triggers downstream processing.
# event_sourcing.py
# Event sourcing pattern with DynamoDB Streams
import json
import boto3
from datetime import datetime
dynamodb = boto3.resource('dynamodb')
events_table = dynamodb.Table('dodatech-account-events')
def record_event(event_type, aggregate_id, data):
"""Append an event to the event store."""
event = {
'eventId': f"{aggregate_id}#{datetime.now().timestamp()}",
'aggregateId': aggregate_id,
'eventType': event_type,
'data': data,
'timestamp': datetime.now().isoformat(),
'version': 1,
}
events_table.put_item(Item=event)
return event
def rebuild_aggregate(aggregate_id):
"""Rebuild current state from all events for an aggregate."""
response = events_table.query(
KeyConditionExpression=boto3.dynamodb.conditions.Key('aggregateId').eq(aggregate_id)
)
events = sorted(response['Items'], key=lambda x: x['timestamp'])
state = {'accountId': aggregate_id, 'balance': 0, 'status': 'active'}
for event in events:
if event['eventType'] == 'ACCOUNT_CREATED':
state['owner'] = event['data']['owner']
elif event['eventType'] == 'DEPOSIT':
state['balance'] += event['data']['amount']
elif event['eventType'] == 'WITHDRAWAL':
state['balance'] -= event['data']['amount']
elif event['eventType'] == 'ACCOUNT_CLOSED':
state['status'] = 'closed'
return state
# Simulate event sourcing
print("=== Event Sourcing Simulation ===\n")
record_event('ACCOUNT_CREATED', 'acc-001', {'owner': 'Alice', 'currency': 'USD'})
record_event('DEPOSIT', 'acc-001', {'amount': 1000, 'reference': 'salary'})
record_event('WITHDRAWAL', 'acc-001', {'amount': 200, 'reference': 'atm'})
record_event('DEPOSIT', 'acc-001', {'amount': 500, 'reference': 'invoice'})
state = rebuild_aggregate('acc-001')
print(f"Rebuilt account state:")
print(f" Owner: {state['owner']}")
print(f" Balance: ${state['balance']}")
print(f" Status: {state['status']}")
print(f"\nAll 4 events replayed to compute current balance.")
Expected output:
=== Event Sourcing Simulation ===
Rebuilt account state:
Owner: Alice
Balance: $1300
Status: active
All 4 events replayed to compute current balance.
Common Serverless Mistakes
1. Monolithic Lambda Functions
Writing all logic in a single large Lambda function defeats the purpose of serverless. Decompose into single-purpose functions connected by events.
2. No Dead Letter Queue (DLQ)
Failed events are lost without a DLQ. Configure SQS DLQs or Lambda destinations for failed invocations to capture and retry errors.
3. Synchronous Orchestration
Lambda calling Lambda directly creates tight coupling and cascading timeouts. Use SQS, Step Functions, or EventBridge for async Orchestration.
4. Improper Error Handling
Lambda retries synchronous invocations twice and async events up to 3 times. Idempotent functions prevent duplicate processing.
5. Not Using Provisioned Concurrency for Critical APIs
Cold starts add 200-500ms latency. For latency-sensitive APIs, use Lambda Provisioned Concurrency or CloudFront with Lambda@Edge pre-warming.
Practice Questions
1. What is the fan-out pattern and when would you use it? Fan-out publishes a single event to multiple subscribers simultaneously. Use it when the same event triggers multiple independent processes: sending emails, updating analytics, and logging to audit.
2. What is the difference between synchronous and asynchronous Lambda invocation? Synchronous (API Gateway, Cognito) waits for the function to complete. Asynchronous (S3, SNS, EventBridge) queues the event and returns immediately. Asynchronous supports retries and DLQs.
3. How does DynamoDB Streams enable Event Sourcing? DynamoDB Streams captures every item modification as a stream record. Lambda processes these records in order. The stream serves as the event log, enabling rebuild of current state from events.
4. What is the Lambda best practice for handling failures? Implement idempotent functions with idempotency keys. For async invocations, enable DLQ. For sync invocations, retry with exponential backoff. Use Step Functions for complex retry logic with different backoff rates.
5. Challenge: Design a serverless order processing system where orders arrive via API, must be validated, payment processed, inventory reserved, and notification sent. Each step may fail and needs retry. Step Functions workflow: ValidateOrder -> ProcessPayment -> ReserveInventory -> SendNotification. Each step is a Lambda function. SQS DLQ for failed steps. DynamoDB for order state. SNS for notification fan-out. CloudWatch alarms on Step Functions execution failures.
Mini Project: Serverless Cost Estimator
# serverless_cost.py
# Estimate serverless vs server costs
def estimate_serverless(
requests_per_month: int,
avg_duration_ms: int = 200,
memory_mb: int = 512,
):
# Lambda pricing
gb_seconds = (requests_per_month * avg_duration_ms / 1000) * (memory_mb / 1024)
compute_cost = gb_seconds * 0.0000166667
request_cost = requests_per_month * 0.0000002
total_lambda = compute_cost + request_cost
# Equivalent EC2 cost
if requests_per_month < 100000:
instance_count = 1
elif requests_per_month < 1000000:
instance_count = 2
else:
instance_count = requests_per_month // 500000 + 1
ec2_cost = instance_count * 24.18 # t3.micro on-demand
print("=== Serverless Cost Estimator ===\n")
print(f"Workload: {requests_per_month:,} requests/month, {avg_duration_ms}ms avg, {memory_mb}MB")
print()
print(f"{'Metric':<35} {'Lambda':<20} {'EC2 (t3.micro)'}")
print("=" * 70)
print(f"{'Monthly compute cost':<35} ${total_lambda:<19.2f} ${ec2_cost:.2f}")
print(f"{'Instances needed':<35} {'auto-scaling':<20} {instance_count}")
print(f"{'Max throughput':<35} auto-scaling fixed")
print(f"{'Operational overhead':<35} near-zero patching, monitoring")
savings = ec2_cost - total_lambda
if savings > 0:
print(f"\nLambda saves ${savings:.2f}/month for this workload")
else:
print(f"\nEC2 is ${abs(savings):.2f}/month cheaper for sustained load")
print("=== Serverless Cost Estimator ===\n")
estimate_serverless(10_000_000, 200, 512)
print()
estimate_serverless(100_000, 500, 1024)
Expected output:
=== Serverless Cost Estimator ===
Workload: 10,000,000 requests/month, 200ms avg, 512MB
Metric Lambda EC2 (t3.micro)
======================================================================
Monthly compute cost $3.26 $24.18
Instances needed auto-scaling 1
Max throughput auto-scaling fixed
Operational overhead near-zero patching, monitoring
Lambda saves $20.92/month for this workload
Workload: 100,000 requests/month, 500ms avg, 1024MB
Metric Lambda EC2 (t3.micro)
======================================================================
Monthly compute cost $0.85 $24.18
Instances needed auto-scaling 1
Max throughput auto-scaling fixed
Operational overhead near-zero patching, monitoring
Lambda saves $23.33/month for this workload
Related Concepts
What's Next
You now understand serverless architecture patterns including event-driven, API backends, fan-out, and Event Sourcing. Next, explore cloud-native development for 12-factor app methodology, and cloud disaster recovery for serverless resiliency.
- Practice daily -- Implement a simple S3-triggered Lambda function
- Build a project -- Create a serverless URL shortener with API Gateway, Lambda, and DynamoDB
- Explore related topics -- Check out AWS Step Functions for serverless Orchestration
Remember: every expert was once a beginner. Keep coding!
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro