Serverless changes how we design applications. Here are patterns that work well in serverless environments.
Function Composition#
1// Single-purpose functions
2export const validateOrder = async (event: APIGatewayEvent) => {
3 const order = JSON.parse(event.body!);
4
5 const errors = validateOrderSchema(order);
6 if (errors.length > 0) {
7 return { statusCode: 400, body: JSON.stringify({ errors }) };
8 }
9
10 // Pass to next function via Step Functions or EventBridge
11 await eventBridge.putEvents({
12 Entries: [{
13 Source: 'orders',
14 DetailType: 'OrderValidated',
15 Detail: JSON.stringify(order),
16 }],
17 });
18
19 return { statusCode: 202, body: JSON.stringify({ status: 'processing' }) };
20};
21
22export const processPayment = async (event: EventBridgeEvent) => {
23 const order = event.detail;
24
25 const payment = await stripe.charges.create({
26 amount: order.total,
27 currency: 'usd',
28 source: order.paymentToken,
29 });
30
31 await eventBridge.putEvents({
32 Entries: [{
33 Source: 'payments',
34 DetailType: 'PaymentProcessed',
35 Detail: JSON.stringify({ orderId: order.id, paymentId: payment.id }),
36 }],
37 });
38};
39
40export const fulfillOrder = async (event: EventBridgeEvent) => {
41 const { orderId, paymentId } = event.detail;
42
43 await db.order.update({
44 where: { id: orderId },
45 data: { status: 'fulfilled', paymentId },
46 });
47
48 await sendConfirmationEmail(orderId);
49};Event-Driven Architecture#
1# serverless.yml
2functions:
3 orderCreated:
4 handler: handlers/orders.created
5 events:
6 - eventBridge:
7 pattern:
8 source:
9 - orders
10 detail-type:
11 - OrderCreated
12
13 inventoryReserved:
14 handler: handlers/inventory.reserved
15 events:
16 - eventBridge:
17 pattern:
18 source:
19 - inventory
20 detail-type:
21 - InventoryReserved
22
23 sendNotification:
24 handler: handlers/notifications.send
25 events:
26 - eventBridge:
27 pattern:
28 source:
29 - orders
30 - payments
31 detail-type:
32 - OrderFulfilled
33 - PaymentFailed1// Event publisher
2import { EventBridgeClient, PutEventsCommand } from '@aws-sdk/client-eventbridge';
3
4const eventBridge = new EventBridgeClient({});
5
6async function publishEvent(
7 source: string,
8 detailType: string,
9 detail: object
10): Promise<void> {
11 await eventBridge.send(new PutEventsCommand({
12 Entries: [{
13 Source: source,
14 DetailType: detailType,
15 Detail: JSON.stringify(detail),
16 EventBusName: process.env.EVENT_BUS_NAME,
17 }],
18 }));
19}
20
21// Usage
22await publishEvent('orders', 'OrderCreated', {
23 orderId: '123',
24 userId: 'user_456',
25 items: [{ productId: 'prod_789', quantity: 2 }],
26});Step Functions Orchestration#
1{
2 "Comment": "Order processing workflow",
3 "StartAt": "ValidateOrder",
4 "States": {
5 "ValidateOrder": {
6 "Type": "Task",
7 "Resource": "arn:aws:lambda:region:account:function:validateOrder",
8 "Next": "CheckInventory",
9 "Catch": [{
10 "ErrorEquals": ["ValidationError"],
11 "Next": "OrderFailed"
12 }]
13 },
14 "CheckInventory": {
15 "Type": "Task",
16 "Resource": "arn:aws:lambda:region:account:function:checkInventory",
17 "Next": "ProcessPayment",
18 "Catch": [{
19 "ErrorEquals": ["OutOfStockError"],
20 "Next": "OrderFailed"
21 }]
22 },
23 "ProcessPayment": {
24 "Type": "Task",
25 "Resource": "arn:aws:lambda:region:account:function:processPayment",
26 "Next": "FulfillOrder",
27 "Retry": [{
28 "ErrorEquals": ["PaymentRetryableError"],
29 "IntervalSeconds": 2,
30 "MaxAttempts": 3,
31 "BackoffRate": 2
32 }],
33 "Catch": [{
34 "ErrorEquals": ["PaymentError"],
35 "Next": "RefundAndFail"
36 }]
37 },
38 "FulfillOrder": {
39 "Type": "Task",
40 "Resource": "arn:aws:lambda:region:account:function:fulfillOrder",
41 "End": true
42 },
43 "OrderFailed": {
44 "Type": "Task",
45 "Resource": "arn:aws:lambda:region:account:function:notifyFailure",
46 "End": true
47 },
48 "RefundAndFail": {
49 "Type": "Task",
50 "Resource": "arn:aws:lambda:region:account:function:refund",
51 "Next": "OrderFailed"
52 }
53 }
54}Fan-Out Pattern#
1// Process items in parallel
2export const processOrder = async (event: SQSEvent) => {
3 const order = JSON.parse(event.Records[0].body);
4
5 // Fan out to process each item
6 await Promise.all(
7 order.items.map(item =>
8 sqs.sendMessage({
9 QueueUrl: process.env.ITEM_PROCESSING_QUEUE,
10 MessageBody: JSON.stringify({
11 orderId: order.id,
12 item,
13 }),
14 })
15 )
16 );
17};
18
19export const processItem = async (event: SQSEvent) => {
20 const { orderId, item } = JSON.parse(event.Records[0].body);
21
22 // Process individual item
23 await reserveInventory(item.productId, item.quantity);
24
25 // Check if all items processed
26 const remaining = await db.orderItem.count({
27 where: { orderId, status: 'pending' },
28 });
29
30 if (remaining === 0) {
31 await publishEvent('orders', 'AllItemsProcessed', { orderId });
32 }
33};CQRS with DynamoDB Streams#
1// Write model
2export const createOrder = async (event: APIGatewayEvent) => {
3 const order = JSON.parse(event.body!);
4
5 await dynamodb.put({
6 TableName: 'Orders',
7 Item: {
8 PK: `ORDER#${order.id}`,
9 SK: `ORDER#${order.id}`,
10 ...order,
11 createdAt: new Date().toISOString(),
12 },
13 });
14
15 return { statusCode: 201, body: JSON.stringify(order) };
16};
17
18// Stream processor - updates read model
19export const updateReadModel = async (event: DynamoDBStreamEvent) => {
20 for (const record of event.Records) {
21 if (record.eventName === 'INSERT' || record.eventName === 'MODIFY') {
22 const order = unmarshall(record.dynamodb!.NewImage!);
23
24 // Update Elasticsearch for search
25 await elasticsearch.index({
26 index: 'orders',
27 id: order.id,
28 body: order,
29 });
30
31 // Update aggregation table
32 await dynamodb.update({
33 TableName: 'UserOrderStats',
34 Key: { userId: order.userId },
35 UpdateExpression: 'ADD orderCount :one SET lastOrderDate = :date',
36 ExpressionAttributeValues: {
37 ':one': 1,
38 ':date': order.createdAt,
39 },
40 });
41 }
42 }
43};Saga Pattern#
1// Saga coordinator
2interface SagaStep {
3 execute: () => Promise<void>;
4 compensate: () => Promise<void>;
5}
6
7async function executeSaga(steps: SagaStep[]): Promise<void> {
8 const completedSteps: SagaStep[] = [];
9
10 try {
11 for (const step of steps) {
12 await step.execute();
13 completedSteps.push(step);
14 }
15 } catch (error) {
16 // Compensate in reverse order
17 for (const step of completedSteps.reverse()) {
18 try {
19 await step.compensate();
20 } catch (compensateError) {
21 console.error('Compensation failed:', compensateError);
22 // Log to dead letter queue for manual intervention
23 }
24 }
25 throw error;
26 }
27}
28
29// Usage
30const orderSaga: SagaStep[] = [
31 {
32 execute: () => reserveInventory(orderId),
33 compensate: () => releaseInventory(orderId),
34 },
35 {
36 execute: () => chargePayment(orderId),
37 compensate: () => refundPayment(orderId),
38 },
39 {
40 execute: () => createShipment(orderId),
41 compensate: () => cancelShipment(orderId),
42 },
43];
44
45await executeSaga(orderSaga);Cost Optimization#
1// Batch processing to reduce invocations
2export const processBatch = async (event: SQSEvent) => {
3 // Process up to 10 messages per invocation
4 const results = await Promise.allSettled(
5 event.Records.map(record => processMessage(JSON.parse(record.body)))
6 );
7
8 // Return failures for retry
9 const failures = results
10 .map((result, index) => ({ result, record: event.Records[index] }))
11 .filter(({ result }) => result.status === 'rejected');
12
13 return {
14 batchItemFailures: failures.map(({ record }) => ({
15 itemIdentifier: record.messageId,
16 })),
17 };
18};
19
20// Use reserved concurrency to control costs
21// serverless.yml
22// functions:
23// expensive:
24// handler: handlers.expensive
25// reservedConcurrency: 5 # Max 5 concurrent executions1# Right-size memory allocation
2functions:
3 lightweight:
4 handler: handlers.lightweight
5 memorySize: 128 # MB
6
7 cpuIntensive:
8 handler: handlers.cpuIntensive
9 memorySize: 1024 # More CPU allocated with more memory
10
11 heavyProcessing:
12 handler: handlers.heavy
13 memorySize: 3008 # Max before ARM pricing changes
14 architecture: arm64 # 20% cheaper on ARMBest Practices#
Design:
✓ Single responsibility per function
✓ Use events for async communication
✓ Implement idempotency
✓ Design for failure and retries
Performance:
✓ Minimize cold starts
✓ Keep functions small
✓ Use connection pooling
✓ Cache when possible
Cost:
✓ Right-size memory allocation
✓ Use ARM architecture
✓ Batch process where possible
✓ Set reserved concurrency limits
Observability:
✓ Structured logging
✓ Distributed tracing
✓ Custom metrics
✓ Alert on errors
Conclusion#
Serverless architecture requires different patterns than traditional applications. Embrace event-driven design, use orchestration for complex workflows, and always consider idempotency. Good serverless design leads to scalable, cost-effective applications.