- Published on
Serverless Architecture Patterns for Modern Applications
Serverless Architecture Patterns for Modern Applications
Serverless computing has revolutionized how we build and deploy applications. This guide explores proven serverless patterns, best practices, and real-world implementation strategies for creating scalable, cost-effective applications. ☁️
Core Serverless Patterns
1. API Gateway + Lambda Pattern
// AWS Lambda function
exports.handler = async (event, context) => {
const { httpMethod, path, body, headers } = event
try {
// Route handling
switch (`${httpMethod} ${path}`) {
case 'GET /users':
return await getUsers(event.queryStringParameters)
case 'POST /users':
return await createUser(JSON.parse(body))
case 'GET /users/{id}':
return await getUser(event.pathParameters.id)
default:
return {
statusCode: 404,
body: JSON.stringify({ message: 'Not Found' }),
}
}
} catch (error) {
console.error('Error:', error)
return {
statusCode: 500,
body: JSON.stringify({ message: 'Internal Server Error' }),
}
}
}
const getUsers = async (queryParams = {}) => {
const { limit = 10, offset = 0 } = queryParams
// Database query logic here
const users = await dynamodb
.scan({
TableName: 'Users',
Limit: parseInt(limit),
ExclusiveStartKey: offset ? { id: offset } : undefined,
})
.promise()
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
users: users.Items,
nextToken: users.LastEvaluatedKey?.id,
}),
}
}
2. Event-Driven Architecture
# serverless.yml
service: event-driven-app
provider:
name: aws
runtime: nodejs18.x
functions:
userCreated:
handler: handlers/userCreated.handler
events:
- sns:
arn: arn:aws:sns:us-east-1:123456789:user-events
filterPolicy:
eventType:
- user.created
sendWelcomeEmail:
handler: handlers/emailService.sendWelcome
events:
- sns:
arn: arn:aws:sns:us-east-1:123456789:user-events
filterPolicy:
eventType:
- user.created
updateAnalytics:
handler: handlers/analytics.updateUserStats
events:
- sns:
arn: arn:aws:sns:us-east-1:123456789:user-events
// Event publisher
const AWS = require('aws-sdk')
const sns = new AWS.SNS()
const publishUserEvent = async (eventType, userData) => {
const message = {
eventType,
timestamp: new Date().toISOString(),
data: userData,
}
await sns
.publish({
TopicArn: process.env.USER_EVENTS_TOPIC,
Message: JSON.stringify(message),
MessageAttributes: {
eventType: {
DataType: 'String',
StringValue: eventType,
},
},
})
.promise()
}
// Event handler
exports.handler = async (event) => {
for (const record of event.Records) {
const message = JSON.parse(record.Sns.Message)
switch (message.eventType) {
case 'user.created':
await handleUserCreated(message.data)
break
case 'user.updated':
await handleUserUpdated(message.data)
break
}
}
}
3. CQRS with Serverless
// Command handler (write operations)
exports.commandHandler = async (event) => {
const { command, data } = JSON.parse(event.body)
switch (command) {
case 'CREATE_USER':
return await createUser(data)
case 'UPDATE_USER':
return await updateUser(data)
case 'DELETE_USER':
return await deleteUser(data)
}
}
// Query handler (read operations)
exports.queryHandler = async (event) => {
const { query, parameters } = event.queryStringParameters
switch (query) {
case 'GET_USERS':
return await getUsersView(parameters)
case 'GET_USER_PROFILE':
return await getUserProfileView(parameters)
case 'SEARCH_USERS':
return await searchUsersView(parameters)
}
}
// Read model updater
exports.readModelUpdater = async (event) => {
for (const record of event.Records) {
if (record.eventName === 'INSERT' || record.eventName === 'MODIFY') {
await updateReadModel(record.dynamodb.NewImage)
}
}
}
Data Patterns
DynamoDB Single Table Design
// Single table design for multi-entity data
const TableName = 'AppData'
// User entity
const createUser = async (userData) => {
const user = {
PK: `USER#${userData.id}`,
SK: `USER#${userData.id}`,
GSI1PK: `USER#${userData.email}`,
GSI1SK: `USER#${userData.email}`,
Type: 'User',
...userData,
CreatedAt: new Date().toISOString(),
}
await dynamodb.put({ TableName, Item: user }).promise()
return user
}
// Post entity
const createPost = async (postData) => {
const post = {
PK: `USER#${postData.authorId}`,
SK: `POST#${postData.id}`,
GSI1PK: `POST#${postData.id}`,
GSI1SK: `POST#${postData.id}`,
Type: 'Post',
...postData,
CreatedAt: new Date().toISOString(),
}
await dynamodb.put({ TableName, Item: post }).promise()
return post
}
// Query patterns
const getUserPosts = async (userId) => {
const result = await dynamodb
.query({
TableName,
KeyConditionExpression: 'PK = :pk AND begins_with(SK, :sk)',
ExpressionAttributeValues: {
':pk': `USER#${userId}`,
':sk': 'POST#',
},
})
.promise()
return result.Items
}
Serverless Data Pipeline
// S3 trigger for data processing
exports.processUpload = async (event) => {
for (const record of event.Records) {
const bucket = record.s3.bucket.name
const key = record.s3.object.key
// Process the uploaded file
await processFile(bucket, key)
}
}
const processFile = async (bucket, key) => {
// Download file from S3
const file = await s3.getObject({ Bucket: bucket, Key: key }).promise()
// Process data (e.g., image resizing, CSV parsing)
const processedData = await processData(file.Body)
// Store results
await storeProcessedData(processedData)
// Trigger next step in pipeline
await sqs
.sendMessage({
QueueUrl: process.env.NEXT_STEP_QUEUE,
MessageBody: JSON.stringify({
bucket,
key,
processedAt: new Date().toISOString(),
}),
})
.promise()
}
Authentication and Security
JWT with Lambda Authorizers
// Custom authorizer
exports.authorize = async (event) => {
const token = event.authorizationToken?.replace('Bearer ', '')
if (!token) {
throw new Error('Unauthorized')
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET)
return {
principalId: decoded.sub,
policyDocument: {
Version: '2012-10-17',
Statement: [
{
Action: 'execute-api:Invoke',
Effect: 'Allow',
Resource: event.methodArn,
},
],
},
context: {
userId: decoded.sub,
email: decoded.email,
role: decoded.role,
},
}
} catch (error) {
throw new Error('Unauthorized')
}
}
// Protected function
exports.protectedHandler = async (event, context) => {
const userId = event.requestContext.authorizer.userId
const userRole = event.requestContext.authorizer.role
// Use user context for business logic
return {
statusCode: 200,
body: JSON.stringify({
message: 'Access granted',
userId,
role: userRole,
}),
}
}
Environment-Based Configuration
// Configuration management
const config = {
development: {
dbTable: 'dev-users',
apiUrl: 'https://dev-api.example.com',
logLevel: 'debug',
},
staging: {
dbTable: 'staging-users',
apiUrl: 'https://staging-api.example.com',
logLevel: 'info',
},
production: {
dbTable: 'prod-users',
apiUrl: 'https://api.example.com',
logLevel: 'error',
},
}
const getConfig = () => {
const stage = process.env.STAGE || 'development'
return config[stage]
}
module.exports = { getConfig }
Performance Optimization
Cold Start Mitigation
// Connection reuse
let dbConnection = null
const getDbConnection = async () => {
if (!dbConnection) {
dbConnection = await createConnection()
}
return dbConnection
}
// Provisioned concurrency configuration
const serverless = {
functions: {
api: {
handler: 'handler.api',
provisionedConcurrency: 5, // Keep 5 instances warm
reservedConcurrency: 100, // Limit max concurrent executions
},
},
}
// Lazy loading
const AWS = require('aws-sdk')
let dynamodb = null
const getDynamoDB = () => {
if (!dynamodb) {
dynamodb = new AWS.DynamoDB.DocumentClient()
}
return dynamodb
}
Caching Strategies
// In-memory caching
const cache = new Map()
const getCachedData = async (key, fetchFunction, ttl = 300000) => {
const cached = cache.get(key)
if (cached && Date.now() - cached.timestamp < ttl) {
return cached.data
}
const data = await fetchFunction()
cache.set(key, { data, timestamp: Date.now() })
return data
}
// ElastiCache integration
const redis = require('redis')
const client = redis.createClient({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
})
const cacheGet = async (key) => {
const cached = await client.get(key)
return cached ? JSON.parse(cached) : null
}
const cacheSet = async (key, value, ttl = 3600) => {
await client.setex(key, ttl, JSON.stringify(value))
}
Monitoring and Observability
Structured Logging
// Centralized logging
const logger = {
info: (message, metadata = {}) => {
console.log(
JSON.stringify({
level: 'info',
message,
timestamp: new Date().toISOString(),
requestId: context.awsRequestId,
...metadata,
})
)
},
error: (message, error = {}, metadata = {}) => {
console.error(
JSON.stringify({
level: 'error',
message,
error: {
message: error.message,
stack: error.stack,
name: error.name,
},
timestamp: new Date().toISOString(),
requestId: context.awsRequestId,
...metadata,
})
)
},
}
// Usage in Lambda
exports.handler = async (event, context) => {
logger.info('Function started', { event })
try {
const result = await processEvent(event)
logger.info('Function completed successfully', { result })
return result
} catch (error) {
logger.error('Function failed', error, { event })
throw error
}
}
Custom Metrics
// CloudWatch custom metrics
const AWS = require('aws-sdk')
const cloudwatch = new AWS.CloudWatch()
const putMetric = async (metricName, value, unit = 'Count', dimensions = {}) => {
const params = {
Namespace: 'MyApp/Lambda',
MetricData: [
{
MetricName: metricName,
Value: value,
Unit: unit,
Dimensions: Object.entries(dimensions).map(([Name, Value]) => ({
Name,
Value,
})),
Timestamp: new Date(),
},
],
}
await cloudwatch.putMetricData(params).promise()
}
// Usage
await putMetric('UserCreated', 1, 'Count', { Environment: 'production' })
await putMetric('ProcessingTime', duration, 'Milliseconds')
Deployment Patterns
Infrastructure as Code
# serverless.yml
service: serverless-app
provider:
name: aws
runtime: nodejs18.x
environment:
STAGE: ${self:provider.stage}
REGION: ${self:provider.region}
functions:
api:
handler: src/handlers/api.handler
events:
- http:
path: /{proxy+}
method: ANY
cors: true
resources:
Resources:
UsersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:service}-${self:provider.stage}-users
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
plugins:
- serverless-webpack
- serverless-offline
- serverless-domain-manager
CI/CD Pipeline
# .github/workflows/deploy.yml
name: Deploy Serverless App
on:
push:
branches: [main, staging]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Deploy to staging
if: github.ref == 'refs/heads/staging'
run: npx serverless deploy --stage staging
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Deploy to production
if: github.ref == 'refs/heads/main'
run: npx serverless deploy --stage production
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
Best Practices
- Function Design: Keep functions small and focused
- Cold Starts: Minimize dependencies and use provisioned concurrency
- Error Handling: Implement proper error boundaries and retries
- Security: Use least privilege principles and environment variables
- Monitoring: Implement comprehensive logging and metrics
- Testing: Write unit tests and integration tests
- Cost Optimization: Monitor usage and optimize resource allocation
Conclusion
Serverless architecture offers powerful patterns for building scalable, cost-effective applications. Key benefits include:
- Automatic Scaling: Handle traffic spikes without manual intervention
- Cost Efficiency: Pay only for actual usage
- Reduced Operations: Less infrastructure management
- Faster Development: Focus on business logic over infrastructure
Success with serverless requires understanding these patterns and applying them appropriately to your use case. Start small, monitor performance, and iterate based on real-world usage patterns. ☁️