Serverless and Microservices Architecture
Microservices Decomposition and Serverless
Microservices Decomposition Principles:
- Single Responsibility: Each service handles one business capability
- Independent Deployment: Services can be released and scaled independently
- Clear Boundaries: Service contracts are defined through APIs
- Replaceability: Individual services can be replaced without affecting the system
Serverless-Friendly Decomposition Patterns:
- Domain-Driven Design (DDD):
- Divide services by bounded contexts
- Example: An e-commerce system can be split into order, payment, and inventory services
- Functional Layer Decomposition:
graph TD A[API Gateway] --> B[User Service] A --> C[Product Service] A --> D[Order Service] B --> E[User Database] C --> F[Product Database] D --> G[Order Database]
Serverless Decomposition Practice:
# serverless.yml example - Order Service
service: order-service
provider:
name: aws
runtime: nodejs14.x
functions:
createOrder:
handler: handler.createOrder
events:
- http:
path: orders
method: post
getOrder:
handler: handler.getOrder
events:
- http:
path: orders/{id}
method: get
resources:
Resources:
OrdersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: Orders
AttributeDefinitions:
- AttributeName: orderId
AttributeType: S
KeySchema:
- AttributeName: orderId
KeyType: HASH
BillingMode: PAY_PER_REQUEST
Decomposition Benefits:
- Independent scaling (automatic scaling on demand)
- Fault isolation (failure in one service doesn’t affect the system)
- Technology heterogeneity (services can use different languages/frameworks)
Service Discovery and Communication
Service Discovery Solutions:
- API Gateway Integration:
# Use API Gateway as a unified entry point
provider:
name: aws
apiGateway:
restApiId: abc123
restApiRootResourceId: /myresource
- Service Registry:
// Use AWS SSM to store service endpoints
const AWS = require('aws-sdk');
const ssm = new AWS.SSM();
async function getServiceEndpoint(serviceName) {
const params = {
Name: `/services/${serviceName}/endpoint`,
WithDecryption: true
};
const result = await ssm.getParameter(params).promise();
return result.Parameter.Value;
}
- Event-Driven Discovery:
# Use EventBridge for inter-service communication
functions:
orderCreatedHandler:
handler: handler.orderCreated
events:
- eventbridge:
eventBusName: orders-event-bus
pattern:
source:
- "order.service"
detail-type:
- "OrderCreated"
Communication Patterns:
| Pattern | Use Case | Example |
|---|---|---|
| Synchronous API | Requires immediate response | Return order ID after user creation |
| Asynchronous Events | Decouple services | Trigger inventory check after order creation |
| Request/Response | RPC-style interaction | Direct calls between microservices |
| Publish/Subscribe | Broadcast notifications | Notify all parties of order status change |
Serverless Micro-Frontend Support
BFF (Backend for Frontend) Pattern:
# Provide different APIs for web and mobile
service: web-bff
functions:
getUserData:
handler: handler.getUserData
events:
- http:
path: web/users/{id}
method: get
service: mobile-bff
functions:
getUserData:
handler: handler.getUserData
events:
- http:
path: mobile/users/{id}
method: get
Micro-Frontend Architecture Integration:
- Independent Deployment:
# Build and deploy Web BFF
cd web-bff && serverless deploy --stage prod
# Build and deploy Mobile BFF
cd mobile-bff && serverless deploy --stage prod
- Frontend Integration:
// Web API calls
const webApi = axios.create({
baseURL: 'https://web-bff-prod.execute-api.us-east-1.amazonaws.com'
});
// Mobile API calls
const mobileApi = axios.create({
baseURL: 'https://mobile-bff-prod.execute-api.us-east-1.amazonaws.com'
});
State Synchronization Solution:
// Use EventBridge to sync state changes
function syncStateChange(event) {
return eventbridge.putEvents({
Entries: [{
Source: 'microfrontend.state.change',
DetailType: 'UserProfileUpdated',
Detail: JSON.stringify(event),
EventBusName: 'state-sync-bus'
}]
}).promise();
}
Serverless and Event Sourcing
Event Sourcing Design
Core Concepts:
- Event: Records all state changes
- Event Stream: A sequence of events ordered by time
- Aggregate Root: A logical unit that reconstructs current state from events
Design Patterns:
- Event Storage Structure:
{
"eventId": "evt_12345",
"eventType": "OrderCreated",
"aggregateId": "order_67890",
"timestamp": "2023-01-01T12:00:00Z",
"data": {
"orderId": "67890",
"customerId": "cust_123",
"items": [...]
},
"metadata": {
"userId": "user_456",
"correlationId": "corr_789"
}
}
- Event Versioning:
{
"eventId": "evt_12345",
"eventType": "OrderUpdated:v2",
"aggregateId": "order_67890",
"timestamp": "2023-01-01T12:05:00Z",
"data": {
"version": 2,
"status": "shipped"
}
}
Serverless Event Storage
Storage Solutions:
- Amazon DynamoDB:
resources:
Resources:
EventsTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: Events
AttributeDefinitions:
- AttributeName: eventId
AttributeType: S
- AttributeName: aggregateId
AttributeType: S
KeySchema:
- AttributeName: eventId
KeyType: HASH
- AttributeName: aggregateId
KeyType: RANGE
BillingMode: PAY_PER_REQUEST
StreamSpecification:
StreamViewType: NEW_IMAGE
- Amazon S3 Event Logs:
resources:
Resources:
EventLogBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: event-log-bucket
VersioningConfiguration:
Status: Enabled
Event Publishing Example:
const AWS = require('aws-sdk');
const dynamodb = new AWS.DynamoDB.DocumentClient();
async function publishEvent(event) {
const params = {
TableName: process.env.EVENTS_TABLE,
Item: {
eventId: generateUUID(),
eventType: event.type,
aggregateId: event.aggregateId,
timestamp: new Date().toISOString(),
data: event.data,
metadata: event.metadata
}
};
await dynamodb.put(params).promise();
// Trigger subsequent processing
await triggerEventProcessors(event);
}
Event Replay and State Reconstruction
Replay Mechanism:
async function rebuildAggregate(aggregateId) {
const params = {
TableName: process.env.EVENTS_TABLE,
KeyConditionExpression: 'aggregateId = :id',
ExpressionAttributeValues: {
':id': aggregateId
},
ScanIndexForward: true // Chronological order
};
const result = await dynamodb.query(params).promise();
let aggregate = { id: aggregateId, state: {} };
for (const event of result.Items) {
aggregate = applyEvent(aggregate, event);
}
return aggregate;
}
function applyEvent(aggregate, event) {
switch(event.eventType) {
case 'OrderCreated':
return { ...aggregate, state: { ...event.data } };
case 'OrderUpdated':
return { ...aggregate, state: { ...aggregate.state, ...event.data } };
// Additional event handling...
default:
return aggregate;
}
}
Replay Optimizations:
- Incremental Replay: Replay only events from a specific time range
- Snapshot Mechanism: Periodically save aggregate state snapshots
async function rebuildWithSnapshot(aggregateId) {
const snapshot = await getLatestSnapshot(aggregateId);
const events = await getEventsSince(snapshot.version, aggregateId);
let aggregate = snapshot.state;
for (const event of events) {
aggregate = applyEvent(aggregate, event);
}
return aggregate;
}
Serverless and Distributed Systems
Distributed Transactions and Consistency
Solutions:
- Saga Pattern:
// Order creation Saga
async function createOrderSaga(orderData) {
try {
const paymentTx = await startPaymentTransaction(orderData.amount);
const order = await createOrder(orderData, paymentTx.id);
await confirmPayment(paymentTx.id);
return order;
} catch (paymentError) {
await cancelPayment(paymentTx.id);
await cancelOrder(orderData.id);
throw paymentError;
}
}
- Eventual Consistency with Events:
functions:
orderCreatedHandler:
handler: handler.orderCreated
events:
- eventbridge:
eventBusName: order-events
pattern:
source:
- "order.service"
detail-type:
- "OrderCreated"
inventoryUpdateHandler:
handler: handler.inventoryUpdate
events:
- eventbridge:
eventBusName: order-events
pattern:
source:
- "order.service"
detail-type:
- "InventoryReserved"
Serverless Task Coordination
Coordination Patterns:
- Step Functions:
# serverless.yml with Step Functions integration
functions:
orderWorkflow:
handler: handler.orderWorkflow
events:
- http:
path: orders
method: post
stepFunctions:
stateMachines:
orderProcessing:
name: OrderProcessingStateMachine
definition:
StartAt: ValidateOrder
States:
ValidateOrder:
Type: Task
Resource: arn:aws:lambda:us-east-1:123456789012:function:validateOrder
Next: ReserveInventory
ReserveInventory:
Type: Task
Resource: arn:aws:lambda:us-east-1:123456789012:function:reserveInventory
Next: ProcessPayment
ProcessPayment:
Type: Task
Resource: arn:aws:lambda:us-east-1:123456789012:function:processPayment
End: true
- Custom Coordinator:
// Simple task coordinator
class TaskCoordinator {
constructor() {
this.tasks = [];
this.currentTaskIndex = 0;
}
addTask(task) {
this.tasks.push(task);
}
async execute() {
while (this.currentTaskIndex < this.tasks.length) {
try {
await this.tasks[this.currentTaskIndex]();
this.currentTaskIndex++;
} catch (error) {
// Error handling logic
break;
}
}
}
}
Distributed Locks and Race Conditions
Lock Implementation Solutions:
- DynamoDB Conditional Writes:
async function acquireLock(lockId, ownerId, ttl) {
const params = {
TableName: process.env.LOCKS_TABLE,
Item: {
lockId,
ownerId,
expiresAt: new Date(Date.now() + ttl).toISOString(),
createdAt: new Date().toISOString()
},
ConditionExpression: 'attribute_not_exists(lockId)'
};
try {
await dynamodb.put(params).promise();
return true;
} catch (error) {
if (error.code === 'ConditionalCheckFailedException') {
return false; // Lock already acquired
}
throw error;
}
}
- Redis Distributed Locks:
const Redis = require('ioredis');
const redis = new Redis();
async function acquireLock(lockKey, timeout = 10000) {
const identifier = Math.random().toString(36).substr(2, 10);
const result = await redis.set(lockKey, identifier, 'PX', timeout, 'NX');
if (result === 'OK') {
return identifier;
}
return null;
}
async function releaseLock(lockKey, identifier) {
const script = `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
`;
return await redis.eval(script, 1, lockKey, identifier);
}
Lock Usage Pattern:
async function processCriticalSection(resourceId) {
const lockId = await acquireLock(`lock:${resourceId}`);
if (!lockId) {
throw new Error('Could not acquire lock');
}
try {
// Execute critical section code
await updateResource(resourceId);
} finally {
await releaseLock(`lock:${resourceId}`, lockId);
}
}



