Skip to content
All essays
CraftMarch 15, 202515 min

Serverless Architecture Patterns: Event-Driven & Microservices

Master modern serverless architecture patterns. Build scalable event-driven systems and microservices with 2025 best practices.

Ü
Ümit Uz
Mobile & Full Stack Developer

Introduction to Architecture Patterns

Serverless architecture patterns have evolved significantly in 2025. Modern applications require scalable, resilient, and cost-effective designs. Let's explore the most powerful patterns shaping serverless development today.

Event-Driven Architecture

Core Concepts

Event-driven architecture (EDA) is the foundation of modern serverless systems. Services communicate asynchronously through events, enabling loose coupling and independent scaling.

Traditional Request/Response:
Client → API Gateway → Lambda → Database
       ← Response     ←       ←

Event-Driven:
Producer → Event Bus → Consumer 1
                 → → Consumer 2
                 → → Consumer 3

AWS EventBridge Implementation

typescript
// producers/orderCreated.ts
import { EventBridgeClient, PutEventsCommand } from '@aws-sdk/client-eventbridge';

const eventBridge = new EventBridgeClient({});

interface OrderEvent {
  orderId: string;
  customerId: string;
  amount: number;
  items: Array<{ productId: string; quantity: number }>;
}

export const handler = async (order: OrderEvent) => {
  const params = {
    Entries: [{
      Source: 'com.myapp.orders',
      DetailType: 'OrderCreated',
      Detail: JSON.stringify({
        orderId: order.orderId,
        customerId: order.customerId,
        amount: order.amount,
        timestamp: new Date().toISOString()
      }),
      EventBusName: process.env.EVENT_BUS_NAME
    }]
  };

  await eventBridge.send(new PutEventsCommand(params));

  return { statusCode: 201, body: JSON.stringify({ message: 'Event published' }) };
};

Event Consumers Pattern

typescript
// consumers/inventoryService.ts
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, UpdateCommand } from '@aws-sdk/lib-dynamodb';

const docClient = DynamoDBDocumentClient.from(new DynamoDBClient({}));

export const handler = async (event: any) => {
  for (const record of event.records) {
    const orderEvent = JSON.parse(record.body);

    // Update inventory
    for (const item of orderEvent.detail.items) {
      await docClient.send(new UpdateCommand({
        TableName: process.env.INVENTORY_TABLE,
        Key: { productId: item.productId },
        UpdateExpression: 'SET quantity = quantity - :qty',
        ExpressionAttributeValues: {
          ':qty': item.quantity
        }
      }));
    }
  }
};

Fan-Out Pattern

typescript
// patterns/fanOut.ts
import { SNSClient, PublishCommand } from '@aws-sdk/client-sns';

const sns = new SNSClient({});

export const handler = async (event: any) => {
  const message = event.detail;

  // Fan out to multiple subscribers
  const promises = [
    sns.send(new PublishCommand({
      TopicArn: process.env.EMAIL_TOPIC_ARN,
      Message: JSON.stringify(message)
    })),
    sns.send(new PublishCommand({
      TopicArn: process.env.ANALYTICS_TOPIC_ARN,
      Message: JSON.stringify(message)
    })),
    sns.send(new PublishCommand({
      TopicArn: process.env.NOTIFICATION_TOPIC_ARN,
      Message: JSON.stringify(message)
    }))
  ];

  await Promise.all(promises);
  return { status: 'success' };
};

Microservices Patterns

Strangler Fig Pattern

Gradually replace monolithic functionality with serverless microservices.

typescript
// patterns/stranglerFig.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';

const MONOLITH_URL = process.env.MONOLITH_URL;

export const handler = async (
  event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
  const { path, httpMethod } = event;

  // Route 1: New serverless endpoint
  if (path === '/api/v2/users' && httpMethod === 'POST') {
    return await createUser(event);
  }

  // Route 2: Migrated endpoint
  if (path === '/api/v2/orders' && httpMethod === 'GET') {
    return await getOrders(event);
  }

  // Route 3: Forward to monolith (not yet migrated)
  const response = await fetch(`${MONOLITH_URL}${path}`, {
    method: httpMethod,
    headers: event.headers as any,
    body: event.body
  });

  return {
    statusCode: response.status,
    body: await response.text()
  };
};

async function createUser(event: APIGatewayProxyEvent) {
  // New serverless implementation
  const body = JSON.parse(event.body || '{}');
  // Create user logic...
  return {
    statusCode: 201,
    body: JSON.stringify({ userId: 'new-user-123' })
  };
}

async function getOrders(event: APIGatewayProxyEvent) {
  // Migrated implementation
  return {
    statusCode: 200,
    body: JSON.stringify({ orders: [] })
  };
}

API Gateway Pattern

yaml
# serverless.yml
service: microservices-api

provider:
  name: aws
  runtime: nodejs20.x
  stage: ${opt:stage, 'dev'}

functions:
  # User Service
  userService:
    handler: services/user/handler.handler
    events:
      - httpApi:
          path: /users/{proxy+}
          method: ANY
    environment:
      TABLE_NAME: UsersTable

  # Order Service
  orderService:
    handler: services/order/handler.handler
    events:
      - httpApi:
          path: /orders/{proxy+}
          method: ANY
    environment:
      TABLE_NAME: OrdersTable

  # Notification Service
  notificationService:
    handler: services/notification/handler.handler
    events:
      - httpApi:
          path: /notifications/{proxy+}
          method: ANY
    environment:
      SNS_TOPIC_ARN: ${self:custom.notificationTopic}

custom:
  notificationTopic:
    Fn::GetAtt: [NotificationTopic, Arn]

resources:
  Resources:
    NotificationTopic:
      Type: AWS::SNS::Topic
      Properties:
        TopicName: ${self:service}-notifications

Service Discovery Pattern

typescript
// patterns/serviceDiscovery.ts
import { SSMMClient, GetParameterCommand } from '@aws-sdk/client-ssm';

const ssm = new SSMClient({});

interface ServiceRegistry {
  userService: string;
  orderService: string;
  notificationService: string;
}

async function getServiceEndpoints(): Promise<ServiceRegistry> {
  const getParam = (name: string) =>
    ssm.send(new GetParameterCommand({
      Name: `/services/${name}/endpoint`,
      WithDecryption: true
    }));

  const [user, order, notification] = await Promise.all([
    getParam('user-service'),
    getParam('order-service'),
    getParam('notification-service')
  ]);

  return {
    userService: user.Parameter.Value,
    orderService: order.Parameter.Value,
    notificationService: notification.Parameter.Value
  };
}

export const handler = async (event: any) => {
  const services = await getServiceEndpoints();

  // Call other services
  const userData = await fetch(services.userService, {
    method: 'POST',
    body: JSON.stringify(event)
  });

  return userData.json();
};

CQRS Pattern

Command Query Responsibility Segregation separates read and write operations.

typescript
// patterns/crs.ts
// Command Side (Write)
interface CreateProductCommand {
  name: string;
  price: number;
  description: string;
}

export const createProduct = async (command: CreateProductCommand) => {
  // Write to primary database
  const product = {
    id: crypto.randomUUID(),
    ...command,
    createdAt: new Date().toISOString()
  };

  await dynamoDb.put({
    TableName: 'Products',
    Item: product
  });

  // Emit event
  await eventBridge.put({
    Source: 'com.myapp.products',
    DetailType: 'ProductCreated',
    Detail: JSON.stringify(product)
  });

  return product;
};

// Query Side (Read)
export const getProducts = async (filters: any) => {
  // Query from read-optimized view
  const result = await dynamoDb.query({
    TableName: 'ProductsView',
    IndexName: 'ByCategoryAndPrice',
    KeyConditionExpression: 'category = :cat',
    FilterExpression: 'price BETWEEN :min AND :max',
    ExpressionAttributeValues: {
      ':cat': filters.category,
      ':min': filters.minPrice,
      ':max': filters.maxPrice
    }
  });

  return result.Items;
};

Saga Pattern

Manage distributed transactions across multiple services.

typescript
// patterns/saga.ts
interface OrderSaga {
  orderId: string;
  state: 'STARTED' | 'PAYMENT_PROCESSING' | 'INVENTORY_RESERVED' | 'COMPLETED' | 'FAILED';
  compensatingActions: Array<{
    service: string;
    action: string;
    payload: any;
  }>;
}

export const orderSagaOrchestrator = async (order: any) => {
  const saga: OrderSaga = {
    orderId: order.orderId,
    state: 'STARTED',
    compensatingActions: []
  };

  try {
    // Step 1: Process payment
    saga.state = 'PAYMENT_PROCESSING';
    const payment = await processPayment(order);
    saga.compensatingActions.push({
      service: 'payment',
      action: 'refund',
      payload: { paymentId: payment.id }
    });

    // Step 2: Reserve inventory
    saga.state = 'INVENTORY_RESERVED';
    await reserveInventory(order.items);
    saga.compensatingActions.push({
      service: 'inventory',
      action: 'release',
      payload: { items: order.items }
    });

    // Step 3: Create order
    await createOrderRecord(order);
    saga.state = 'COMPLETED';

    return saga;
  } catch (error) {
    // Execute compensating transactions
    await executeCompensatingActions(saga.compensatingActions);
    saga.state = 'FAILED';
    throw error;
  }
};

async function executeCompensatingActions(actions: any[]) {
  for (const action of actions.reverse()) {
    try {
      await fetch(`${process.env.API_BASE}/${action.service}/${action.action}`, {
        method: 'POST',
        body: JSON.stringify(action.payload)
      });
    } catch (error) {
      console.error('Compensating action failed:', error);
    }
  }
}

Step Functions Pattern

Orchestrate complex workflows with AWS Step Functions.

typescript
// patterns/stepFunctions.ts
import { SFNClient, StartExecutionCommand } from '@aws-sdk/client-sfn';

const sfn = new SFNClient({});

export const handler = async (order: any) => {
  const params = {
    stateMachineArn: process.env.ORDER_PROCESSOR_ARN,
    input: JSON.stringify({
      orderId: order.orderId,
      customerId: order.customerId,
      items: order.items,
      timestamp: new Date().toISOString()
    }),
    name: `order-${order.orderId}`
  };

  const execution = await sfn.send(new StartExecutionCommand(params));

  return {
    statusCode: 202,
    body: JSON.stringify({
      executionArn: execution.executionArn,
      message: 'Order processing started'
    })
  };
};

Step Functions Definition

yaml
# stepfunctions/orderProcessor.asl.json
{
  "Comment": "Order processing workflow",
  "StartAt": "ValidateOrder",
  "States": {
    "ValidateOrder": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:REGION:ACCOUNT_ID:function:ValidateOrder",
      "Next": "ProcessPayment",
      "Catch": [{
        "ErrorEquals": ["States.ALL"],
        "ResultPath": "$.error",
        "Next": "NotifyFailure"
      }]
    },
    "ProcessPayment": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:REGION:ACCOUNT_ID:function:ProcessPayment",
      "Next": "ReserveInventory",
      "Catch": [{
        "ErrorEquals": ["States.ALL"],
        "ResultPath": "$.error",
        "Next": "RefundPayment"
      }]
    },
    "ReserveInventory": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:REGION:ACCOUNT_ID:function:ReserveInventory",
      "Next": "ConfirmOrder",
      "Catch": [{
        "ErrorEquals": ["States.ALL"],
        "ResultPath": "$.error",
        "Next": "ReleaseInventory"
      }]
    },
    "ConfirmOrder": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:REGION:ACCOUNT_ID:function:ConfirmOrder",
      "Next": "NotifySuccess",
      "Catch": [{
        "ErrorEquals": ["States.ALL"],
        "ResultPath": "$.error",
        "Next": "NotifyFailure"
      }]
    },
    "NotifySuccess": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:REGION:ACCOUNT_ID:function:NotifySuccess",
      "End": true
    },
    "RefundPayment": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:REGION:ACCOUNT_ID:function:RefundPayment",
      "Next": "NotifyFailure"
    },
    "ReleaseInventory": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:REGION:ACCOUNT_ID:function:ReleaseInventory",
      "Next": "RefundPayment"
    },
    "NotifyFailure": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:REGION:ACCOUNT_ID:function:NotifyFailure",
      "End": true
    }
  }
}

Backend For Frontend (BFF) Pattern

Create dedicated APIs for different client types.

typescript
// patterns/bff.ts
// Web BFF
export const webBFF = async (event: APIGatewayProxyEvent) => {
  const user = await getUserData(event);
  const orders = await getUserOrders(user.id);
  const recommendations = await getRecommendations(user.id);

  // Transform for web client
  return {
    statusCode: 200,
    body: JSON.stringify({
      user: {
        name: user.name,
        email: user.email,
        avatar: user.avatar
      },
      orders: orders.map(o => ({
        id: o.id,
        total: o.total,
        status: o.status,
        date: o.createdAt
      })),
      recommendations: recommendations.slice(0, 5)
    })
  };
};

// Mobile BFF
export const mobileBFF = async (event: APIGatewayProxyEvent) => {
  const user = await getUserData(event);
  const orders = await getUserOrders(user.id);

  // Transform for mobile client (lighter payload)
  return {
    statusCode: 200,
    body: JSON.stringify({
      u: {
        n: user.name,
        a: user.avatar
      },
      o: orders.map(o => ({
        i: o.id,
        t: o.total,
        s: o.status[0] // First letter only
      }))
    })
  };
};

Cache-Aside Pattern

Implement intelligent caching strategies.

typescript
// patterns/cacheAside.ts
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, GetCommand } from '@aws-sdk/lib-dynamodb';

const docClient = DynamoDBDocumentClient.from(new DynamoDBClient({}));

export const handler = async (event: any) => {
  const { productId } = event.pathParameters;
  const cacheKey = `product:${productId}`;

  // Try cache first
  const cachedItem = await redis.get(cacheKey);
  if (cachedItem) {
    return {
      statusCode: 200,
      headers: { 'X-Cache': 'HIT' },
      body: cachedItem
    };
  }

  // Cache miss - fetch from database
  const result = await docClient.send(new GetCommand({
    TableName: 'Products',
    Key: { id: productId }
  }));

  if (!result.Item) {
    return { statusCode: 404, body: 'Not found' };
  }

  // Store in cache for 5 minutes
  await redis.setex(cacheKey, 300, JSON.stringify(result.Item));

  return {
    statusCode: 200,
    headers: { 'X-Cache': 'MISS' },
    body: JSON.stringify(result.Item)
  });
};

Best Practices

1. Idempotency

typescript
export const idempotentHandler = async (event: any) => {
  const idempotencyKey = event.headers['Idempotency-Key'];

  // Check if already processed
  const existing = await dynamoDb.get({
    TableName: 'IdempotencyStore',
    Key: { idempotencyKey }
  });

  if (existing.Item) {
    return JSON.parse(existing.Item.response);
  }

  // Process request
  const result = await processBusinessLogic(event);

  // Store result
  await dynamoDb.put({
    TableName: 'IdempotencyStore',
    Item: {
      idempotencyKey,
      response: JSON.stringify(result),
      expiration: Math.floor(Date.now() / 1000) + 86400 // 24 hours
    }
  });

  return result;
};

2. Dead Letter Queues

yaml
functions:
  processor:
    handler: handler.process
    events:
      - sqs:
          arn: !GetAtt InputQueue.Arn
    deadLetterArn: !GetAtt DLQ.Arn
    maximumRetryAttempts: 3

3. Circuit Breaker

typescript
class CircuitBreaker {
  private failures = 0;
  private lastFailureTime = 0;
  private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';

  constructor(
    private threshold = 5,
    private timeout = 60000 // 1 minute
  ) {}

  async execute(fn: Function) {
    if (this.state === 'OPEN') {
      if (Date.now() - this.lastFailureTime > this.timeout) {
        this.state = 'HALF_OPEN';
      } else {
        throw new Error('Circuit breaker is OPEN');
      }
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  private onSuccess() {
    this.failures = 0;
    this.state = 'CLOSED';
  }

  private onFailure() {
    this.failures++;
    this.lastFailureTime = Date.now();
    if (this.failures >= this.threshold) {
      this.state = 'OPEN';
    }
  }
}

Monitoring & Observability

typescript
import { CloudWatchMetrics } from '@aws-lambda-powertools/metrics';

const metrics = new CloudWatchMetrics({
  namespace: 'ServerlessApp',
  serviceName: 'OrderService'
});

export const handler = async (event: any) => {
  metrics.addMetric('SuccessfulOrders', 'Count', 1);

  try {
    // Business logic
    metrics.addMetric('ProcessingTime', 'Value', Date.now() - startTime);
  } catch (error) {
    metrics.addMetric('FailedOrders', 'Count', 1);
    throw error;
  }

  metrics.publishStoredMetrics();
};

Conclusion

Serverless architecture patterns in 2025 focus on:

  • Event-driven communication for loose coupling
  • Microservices for independent scalability
  • Orchestration for complex workflows
  • Resilience through patterns like saga and circuit breaker

These patterns enable building production-grade serverless applications that are scalable, maintainable, and cost-effective.

Start with simple patterns and evolve as your application grows!

Related essays

Next essay
Machine Learning with TensorFlow.js