Cloud Architecture Patterns: Microservices and Serverless
Explore modern cloud architecture patterns including microservices, serverless computing, event-driven architecture, and cloud-native design principles.
Table of Contents
Cloud Architecture Patterns: Microservices and Serverless
Modern cloud applications require architectures that can scale, be resilient, and adapt to changing requirements. This guide explores key cloud architecture patterns and their implementation.
Microservices Architecture
Service Decomposition
Break down monolithic applications into smaller, independent services:
// User Service
class UserService {
constructor(
private readonly userRepository: UserRepository,
private readonly eventPublisher: EventPublisher,
) {}
async createUser(userData: CreateUserRequest): Promise<User> {
const user = await this.userRepository.create(userData);
await this.eventPublisher.publish("UserCreated", {
userId: user.id,
email: user.email,
});
return user;
}
}
// Order Service
class OrderService {
constructor(
private readonly orderRepository: OrderRepository,
private readonly userService: UserServiceClient,
private readonly inventoryService: InventoryServiceClient,
) {}
async createOrder(orderData: CreateOrderRequest): Promise<Order> {
// Validate user exists
const user = await this.userService.getUser(orderData.userId);
// Check inventory
await this.inventoryService.reserveItems(orderData.items);
// Create order
const order = await this.orderRepository.create({
...orderData,
status: "pending",
});
return order;
}
}API Gateway Pattern
Centralize API management and routing:
// API Gateway configuration
const gateway = new APIGateway();
gateway
.route("/api/users", "user-service:3001")
.route("/api/orders", "order-service:3002")
.middleware(authenticate)
.middleware(rateLimit)
.middleware(logRequest);
// Service discovery
class ServiceRegistry {
private services = new Map<string, ServiceEndpoint>();
register(serviceName: string, endpoint: ServiceEndpoint) {
this.services.set(serviceName, endpoint);
}
discover(serviceName: string): ServiceEndpoint | undefined {
return this.services.get(serviceName);
}
}Serverless Computing
Function as a Service (FaaS)
Build applications using serverless functions:
// AWS Lambda function
export const handler = async (
event: APIGatewayEvent,
): Promise<APIGatewayProxyResult> => {
try {
const { userId } = event.pathParameters || {};
if (!userId) {
return {
statusCode: 400,
body: JSON.stringify({ error: "User ID is required" }),
};
}
const user = await getUserFromDatabase(userId);
return {
statusCode: 200,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify(user),
};
} catch (error) {
console.error("Error processing request:", error);
return {
statusCode: 500,
body: JSON.stringify({ error: "Internal server error" }),
};
}
};Serverless Orchestration
Coordinate multiple serverless functions:
# AWS Step Functions definition
Comment: "User registration workflow"
StartAt: ValidateUser
States:
ValidateUser:
Type: Task
Resource: arn:aws:lambda:us-east-1:123456789012:function:validate-user
Next: CreateUser
Catch:
- ErrorEquals: ["ValidationError"]
Next: SendValidationError
CreateUser:
Type: Task
Resource: arn:aws:lambda:us-east-1:123456789012:function:create-user
Next: SendWelcomeEmail
ResultPath: "$.user"
SendWelcomeEmail:
Type: Task
Resource: arn:aws:lambda:us-east-1:123456789012:function:send-welcome-email
End: true
InputPath: "$.user"Event-Driven Architecture
Event Sourcing
Store state changes as events:
interface DomainEvent {
eventId: string;
aggregateId: string;
eventType: string;
data: Record<string, any>;
timestamp: Date;
version: number;
}
class EventStore {
async saveEvents(aggregateId: string, events: DomainEvent[]): Promise<void> {
for (const event of events) {
await this.db.collection("events").insertOne({
...event,
aggregateId,
});
}
}
async getEvents(aggregateId: string): Promise<DomainEvent[]> {
return await this.db.collection("events")
.find({ aggregateId })
.sort({ version: 1 })
.toArray();
}
}
class Aggregate {
private events: DomainEvent[] = [];
private version = 0;
protected applyEvent(event: DomainEvent) {
this.events.push(event);
this.version = event.version;
this.apply(event);
}
protected abstract apply(event: DomainEvent): void;
}Event Streaming
Process events in real-time:
// Apache Kafka producer
class EventProducer {
constructor(private kafka: Kafka) {}
async publishEvent(topic: string, event: DomainEvent) {
const producer = this.kafka.producer();
await producer.connect();
await producer.send({
topic,
messages: [{
key: event.aggregateId,
value: JSON.stringify(event),
}],
});
await producer.disconnect();
}
}
// Kafka consumer
class EventConsumer {
constructor(private kafka: Kafka) {}
async consume(topic: string, handler: (event: DomainEvent) => Promise<void>) {
const consumer = this.kafka.consumer({ groupId: "event-handlers" });
await consumer.connect();
await consumer.subscribe({ topic, fromBeginning: true });
await consumer.run({
eachMessage: async ({ message }) => {
const event = JSON.parse(message.value!.toString());
await handler(event);
},
});
}
}Cloud-Native Patterns
Circuit Breaker Pattern
Handle service failures gracefully:
class CircuitBreaker {
private failureCount = 0;
private state: "CLOSED" | "OPEN" | "HALF_OPEN" = "CLOSED";
async execute<T>(operation: () => Promise<T>): Promise<T> {
if (this.state === "OPEN") {
throw new Error("Circuit breaker is OPEN");
}
try {
const result = await operation();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess() {
this.failureCount = 0;
this.state = "CLOSED";
}
private onFailure() {
this.failureCount++;
if (this.failureCount >= this.failureThreshold) {
this.state = "OPEN";
setTimeout(() => {
this.state = "HALF_OPEN";
}, this.timeout);
}
}
}Saga Pattern
Manage distributed transactions:
interface SagaStep {
execute: (context: SagaContext) => Promise<void>;
compensate: (context: SagaContext) => Promise<void>;
}
class SagaOrchestrator {
async executeSaga(saga: SagaStep[], context: SagaContext) {
const executedSteps: SagaStep[] = [];
try {
for (const step of saga) {
await step.execute(context);
executedSteps.push(step);
}
} catch (error) {
// Compensate in reverse order
for (const step of executedSteps.reverse()) {
await step.compensate(context);
}
throw error;
}
}
}
// Usage
const orderSaga: SagaStep[] = [
{
execute: async (ctx) => await inventoryService.reserveItems(ctx.items),
compensate: async (ctx) => await inventoryService.releaseItems(ctx.items),
},
{
execute: async (ctx) => await paymentService.chargeCard(ctx.payment),
compensate: async (ctx) => await paymentService.refundCharge(ctx.payment),
},
{
execute: async (ctx) => await shippingService.createShipment(ctx.order),
compensate: async (ctx) => await shippingService.cancelShipment(ctx.order),
},
];Scalability Patterns
Database Sharding
Distribute data across multiple databases:
class DatabaseSharder {
private shards: DatabaseConnection[] = [];
getShard(key: string): DatabaseConnection {
const shardIndex = this.hash(key) % this.shards.length;
return this.shards[shardIndex];
}
private hash(key: string): number {
let hash = 0;
for (let i = 0; i < key.length; i++) {
const char = key.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32-bit integer
}
return Math.abs(hash);
}
}Caching Strategies
Implement multi-level caching:
class CacheManager {
constructor(
private l1Cache: Cache, // In-memory
private l2Cache: Cache, // Redis
private database: Database,
) {}
async get<T>(key: string): Promise<T | null> {
// Check L1 cache first
let value = await this.l1Cache.get<T>(key);
if (value) return value;
// Check L2 cache
value = await this.l2Cache.get<T>(key);
if (value) {
// Populate L1 cache
await this.l1Cache.set(key, value);
return value;
}
// Fetch from database
value = await this.database.get<T>(key);
if (value) {
// Populate both caches
await Promise.all([
this.l1Cache.set(key, value),
this.l2Cache.set(key, value),
]);
}
return value;
}
}Monitoring and Observability
Distributed Tracing
Track requests across services:
// OpenTelemetry tracing
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
import { SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
import { JaegerExporter } from "@opentelemetry/exporter-jaeger";
const provider = new NodeTracerProvider();
const exporter = new JaegerExporter();
provider.addSpanProcessor(new SimpleSpanProcessor(exporter));
provider.register();
const tracer = provider.getTracer("my-service");
// Usage
const span = tracer.startSpan("process-order");
try {
await processOrder(orderData);
span.setStatus({ code: SpanStatusCode.OK });
} catch (error) {
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
throw error;
} finally {
span.end();
}Service Mesh
Manage service-to-service communication:
# Istio VirtualService
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: user-service
spec:
http:
- match:
- uri:
prefix: "/api/users"
route:
- destination:
host: user-service
timeout: 10s
retries:
attempts: 3
perTryTimeout: 2s
- match:
- uri:
prefix: "/api/admin"
route:
- destination:
host: user-service
corsPolicy:
allowOrigins:
- exact: https://admin.myapp.comCost Optimization
Auto-scaling
Scale resources based on demand:
// AWS Application Auto Scaling
const autoScaling = new AutoScalingClient({ region: "us-east-1" });
await autoScaling.putScalingPolicy({
PolicyName: "cpu-scaling",
ServiceNamespace: "ecs",
ResourceId: "service/my-cluster/my-service",
ScalableDimension: "ecs🐕🦺DesiredCount",
PolicyType: "TargetTrackingScaling",
TargetTrackingScalingPolicyConfiguration: {
TargetValue: 70.0,
PredefinedMetricSpecification: {
PredefinedMetricType: "ECSServiceAverageCPUUtilization",
},
},
});Conclusion
Cloud architecture patterns provide the foundation for building scalable, resilient, and maintainable applications. Choose patterns that align with your business requirements, team capabilities, and long-term goals. Remember that architecture is evolutionary - start simple and evolve as your needs grow.