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 3AWS EventBridge Implementation
// 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
// 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
// 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.
// 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
# 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}-notificationsService Discovery Pattern
// 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.
// 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.
// 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.
// 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
# 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.
// 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.
// 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
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
functions:
processor:
handler: handler.process
events:
- sqs:
arn: !GetAtt InputQueue.Arn
deadLetterArn: !GetAtt DLQ.Arn
maximumRetryAttempts: 33. Circuit Breaker
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
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!