Skip to content

Serverless Architecture Patterns Explained -- Complete Guide

DodaTech Updated 2026-06-22 9 min read

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
AWS Lambda
Serverless Framework
Cloud-Native Development
Azure Functions

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