11 mins
Microservices Architecture Patterns: Building Scalable Distributed Systems

Comprehensive guide to designing, implementing, and managing microservices architectures with real-world patterns and examples

Microservices Architecture Patterns: Building Scalable Distributed Systemsh1

Hello! I’m Ahmet Zeybek, a full stack developer with extensive experience in designing and implementing microservices architectures for large-scale applications. Microservices have transformed how we build software, enabling teams to develop, deploy, and scale services independently. In this comprehensive guide, I’ll share the patterns and practices that have helped me build resilient, scalable distributed systems.

Why Microservices?h2

Microservices architecture addresses the limitations of monolithic applications:

  • Independent Deployment: Deploy services without affecting others
  • Technology Diversity: Use different tech stacks for different services
  • Scalability: Scale individual services based on demand
  • Team Autonomy: Teams can work independently
  • Fault Isolation: Failures are contained to specific services
  • Faster Development: Smaller codebases are easier to understand and maintain

Service Design Patternsh2

1. Domain-Driven Design (DDD) for Service Boundariesh3

Design services around business domains:

// User domain
interface UserService {
createUser(userData: CreateUserRequest): Promise<User>
getUser(userId: string): Promise<User>
updateUser(userId: string, updates: UpdateUserRequest): Promise<User>
deleteUser(userId: string): Promise<void>
authenticate(credentials: LoginRequest): Promise<AuthToken>
}
// Order domain
interface OrderService {
createOrder(orderData: CreateOrderRequest): Promise<Order>
getOrder(orderId: string): Promise<Order>
updateOrderStatus(orderId: string, status: OrderStatus): Promise<Order>
cancelOrder(orderId: string): Promise<void>
getUserOrders(userId: string): Promise<Order[]>
}
// Payment domain
interface PaymentService {
processPayment(paymentData: PaymentRequest): Promise<PaymentResult>
refundPayment(paymentId: string): Promise<RefundResult>
getPaymentStatus(paymentId: string): Promise<PaymentStatus>
}
// Inventory domain
interface InventoryService {
checkStock(productId: string): Promise<StockInfo>
reserveStock(productId: string, quantity: number): Promise<Reservation>
releaseStock(reservationId: string): Promise<void>
updateStock(productId: string, quantity: number): Promise<void>
}

2. API Gateway Patternh3

Central entry point for client requests:

// API Gateway implementation
class APIGateway {
private serviceRegistry: Map<string, ServiceInfo> = new Map()
constructor(private circuitBreaker: CircuitBreaker) {}
async routeRequest(request: APIRequest): Promise<APIResponse> {
// 1. Authentication & Authorization
const user = await this.authenticate(request)
// 2. Rate Limiting
await this.checkRateLimit(user.id)
// 3. Request Routing
const targetService = this.determineTargetService(request.path)
// 4. Circuit Breaker Check
if (this.circuitBreaker.isOpen(targetService.name)) {
throw new ServiceUnavailableError(`${targetService.name} is currently unavailable`)
}
// 5. Load Balancing
const serviceInstance = await this.loadBalancer.selectInstance(targetService)
// 6. Request Transformation
const transformedRequest = this.transformRequest(request, targetService)
// 7. Service Call
const response = await this.callService(serviceInstance, transformedRequest)
// 8. Response Transformation
return this.transformResponse(response)
}
private determineTargetService(path: string): ServiceInfo {
const routeMappings: Record<string, string> = {
'/api/users': 'user-service',
'/api/orders': 'order-service',
'/api/payments': 'payment-service',
'/api/inventory': 'inventory-service',
}
for (const [route, service] of Object.entries(routeMappings)) {
if (path.startsWith(route)) {
return this.serviceRegistry.get(service)!
}
}
throw new NotFoundError('Route not found')
}
}
// Load balancing strategies
class LoadBalancer {
async selectInstance(service: ServiceInfo): Promise<ServiceInstance> {
switch (service.loadBalancingStrategy) {
case 'ROUND_ROBIN':
return this.roundRobin(service.instances)
case 'LEAST_CONNECTIONS':
return this.leastConnections(service.instances)
case 'WEIGHTED_RESPONSE_TIME':
return this.weightedResponseTime(service.instances)
default:
return service.instances[0]
}
}
}

3. Service Mesh Patternh3

Handle service-to-service communication:

# Istio VirtualService configuration
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: user-service
spec:
hosts:
- user-service
http:
- match:
- headers:
x-api-version:
exact: 'v2'
route:
- destination:
host: user-service
subset: v2
- route:
- destination:
host: user-service
subset: v1
---
# Istio DestinationRule
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: user-service
spec:
host: user-service
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2
trafficPolicy:
tls:
mode: ISTIO_MUTUAL
loadBalancer:
simple: ROUND_ROBIN

Communication Patternsh2

4. Synchronous Communication (REST/GraphQL)h3

Direct service-to-service calls:

// Order service calling User service
class OrderService {
constructor(
private userServiceClient: UserServiceClient,
private paymentServiceClient: PaymentServiceClient,
private inventoryServiceClient: InventoryServiceClient
) {}
async createOrder(orderRequest: CreateOrderRequest): Promise<Order> {
// 1. Validate user exists and is active
const user = await this.userServiceClient.getUser(orderRequest.userId)
if (!user) {
throw new ValidationError('User not found')
}
if (!user.isActive) {
throw new ValidationError('User account is inactive')
}
// 2. Check inventory availability
for (const item of orderRequest.items) {
const stockInfo = await this.inventoryServiceClient.checkStock(item.productId)
if (stockInfo.available < item.quantity) {
throw new ValidationError(`Insufficient stock for ${item.productId}`)
}
}
// 3. Reserve inventory
const reservations = []
for (const item of orderRequest.items) {
const reservation = await this.inventoryServiceClient.reserveStock(item.productId, item.quantity)
reservations.push(reservation)
}
try {
// 4. Process payment
const paymentResult = await this.paymentServiceClient.processPayment({
orderId: orderRequest.orderId,
amount: orderRequest.totalAmount,
userId: orderRequest.userId,
paymentMethod: orderRequest.paymentMethod,
})
if (!paymentResult.success) {
throw new PaymentError('Payment processing failed')
}
// 5. Create order
const order = await this.orderRepository.create({
...orderRequest,
status: OrderStatus.PAID,
paymentId: paymentResult.paymentId,
})
return order
} catch (error) {
// Release reserved inventory on failure
for (const reservation of reservations) {
await this.inventoryServiceClient.releaseStock(reservation.id)
}
throw error
}
}
}

5. Asynchronous Communication (Event-Driven)h3

Decoupled communication using events:

// Event definitions
interface OrderCreatedEvent {
type: 'ORDER_CREATED'
data: {
orderId: string
userId: string
totalAmount: number
items: OrderItem[]
}
metadata: {
timestamp: Date
correlationId: string
userId: string
}
}
interface PaymentProcessedEvent {
type: 'PAYMENT_PROCESSED'
data: {
orderId: string
paymentId: string
amount: number
status: PaymentStatus
}
metadata: {
timestamp: Date
correlationId: string
}
}
// Event publisher
class EventPublisher {
constructor(private eventBus: EventBus) {}
async publish(event: DomainEvent): Promise<void> {
await this.eventBus.publish({
topic: this.getTopicForEvent(event.type),
event: {
id: uuidv4(),
type: event.type,
data: event.data,
metadata: {
...event.metadata,
publishedAt: new Date(),
source: 'order-service',
},
},
})
}
private getTopicForEvent(eventType: string): string {
const topicMapping: Record<string, string> = {
ORDER_CREATED: 'orders',
PAYMENT_PROCESSED: 'payments',
INVENTORY_UPDATED: 'inventory',
}
return topicMapping[eventType] || 'default'
}
}
// Event handlers
class InventoryEventHandler {
constructor(
private inventoryService: InventoryService,
private eventBus: EventBus
) {}
async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {
// Reserve inventory for new order
for (const item of event.data.items) {
await this.inventoryService.reserveStock(item.productId, item.quantity)
}
}
async handlePaymentProcessed(event: PaymentProcessedEvent): Promise<void> {
if (event.data.status === PaymentStatus.COMPLETED) {
// Confirm inventory reservation
await this.inventoryService.confirmReservation(event.data.orderId)
} else if (event.data.status === PaymentStatus.FAILED) {
// Release inventory reservation
await this.inventoryService.releaseReservation(event.data.orderId)
}
}
}
// Event bus implementation
class EventBus {
private subscribers: Map<string, EventHandler[]> = new Map()
subscribe(topic: string, handler: EventHandler): void {
if (!this.subscribers.has(topic)) {
this.subscribers.set(topic, [])
}
this.subscribers.get(topic)!.push(handler)
}
async publish(message: EventMessage): Promise<void> {
const handlers = this.subscribers.get(message.topic) || []
await Promise.all(handlers.map((handler) => handler.handle(message.event)))
}
}

Data Management Patternsh2

6. Database per Serviceh3

Each service owns its data:

// User service database schema
const userSchema = {
users: {
id: 'UUID PRIMARY KEY',
email: 'VARCHAR(255) UNIQUE NOT NULL',
password_hash: 'VARCHAR(255) NOT NULL',
first_name: 'VARCHAR(100)',
last_name: 'VARCHAR(100)',
created_at: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP',
updated_at: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP',
},
user_profiles: {
id: 'UUID PRIMARY KEY',
user_id: 'UUID REFERENCES users(id)',
display_name: 'VARCHAR(100)',
bio: 'TEXT',
avatar_url: 'VARCHAR(500)',
location: 'VARCHAR(200)',
website: 'VARCHAR(500)',
},
}
// Order service database schema
const orderSchema = {
orders: {
id: 'UUID PRIMARY KEY',
user_id: 'UUID NOT NULL', // Reference to user (no FK constraint)
total_amount: 'DECIMAL(10,2) NOT NULL',
status: 'VARCHAR(50) NOT NULL',
created_at: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP',
updated_at: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP',
},
order_items: {
id: 'UUID PRIMARY KEY',
order_id: 'UUID REFERENCES orders(id)',
product_id: 'UUID NOT NULL',
product_name: 'VARCHAR(255) NOT NULL', // Denormalized data
quantity: 'INTEGER NOT NULL',
unit_price: 'DECIMAL(10,2) NOT NULL',
},
}

7. CQRS (Command Query Responsibility Segregation)h3

Separate read and write operations:

// Command side (write operations)
class OrderCommandService {
constructor(
private orderRepository: OrderRepository,
private eventPublisher: EventPublisher
) {}
async createOrder(command: CreateOrderCommand): Promise<Order> {
// Business logic validation
const order = new Order(command.userId, command.items, command.shippingAddress)
// Save to write database
const savedOrder = await this.orderRepository.save(order)
// Publish domain event
await this.eventPublisher.publish(new OrderCreatedEvent(savedOrder))
return savedOrder
}
async updateOrderStatus(command: UpdateOrderStatusCommand): Promise<Order> {
const order = await this.orderRepository.findById(command.orderId)
if (!order) {
throw new NotFoundError('Order not found')
}
order.updateStatus(command.status)
const updatedOrder = await this.orderRepository.save(order)
await this.eventPublisher.publish(new OrderStatusUpdatedEvent(updatedOrder))
return updatedOrder
}
}
// Query side (read operations)
class OrderQueryService {
constructor(private readDatabase: ReadDatabase) {}
async getOrder(orderId: string): Promise<OrderView> {
return await this.readDatabase.queryOne(
`
SELECT o.*, c.name as customer_name, c.email as customer_email
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.id = $1
`,
[orderId]
)
}
async getUserOrders(userId: string, filters: OrderFilters): Promise<OrderListView> {
let whereClause = 'WHERE o.customer_id = $1'
const params = [userId]
let paramIndex = 2
if (filters.status) {
whereClause += ` AND o.status = $${paramIndex}`
params.push(filters.status)
paramIndex++
}
if (filters.dateFrom) {
whereClause += ` AND o.created_at >= $${paramIndex}`
params.push(filters.dateFrom)
paramIndex++
}
const orders = await this.readDatabase.query(
`
SELECT o.id, o.total_amount, o.status, o.created_at,
COUNT(oi.id) as item_count
FROM orders o
LEFT JOIN order_items oi ON o.id = oi.order_id
${whereClause}
GROUP BY o.id, o.total_amount, o.status, o.created_at
ORDER BY o.created_at DESC
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
`,
[...params, filters.limit, filters.offset]
)
const totalCount = await this.readDatabase.queryOne(
`
SELECT COUNT(*) as count FROM orders o ${whereClause}
`,
params.slice(0, -2)
)
return {
orders,
totalCount: parseInt(totalCount.count),
hasNextPage: filters.offset + orders.length < parseInt(totalCount.count),
}
}
}

Observability Patternsh2

8. Distributed Tracingh3

Track requests across services:

import { Tracer } from 'opentelemetry'
// Initialize tracing
const tracer = new Tracer({
serviceName: 'order-service',
serviceVersion: '1.0.0',
})
// Traced service method
class OrderService {
async createOrder(orderRequest: CreateOrderRequest): Promise<Order> {
return tracer.startActiveSpan('createOrder', async (span) => {
try {
span.setAttribute('user.id', orderRequest.userId)
span.setAttribute('order.items.count', orderRequest.items.length)
// Validate user
const userSpan = tracer.startSpan('validateUser')
const user = await this.userServiceClient.getUser(orderRequest.userId)
userSpan.end()
if (!user) {
throw new ValidationError('User not found')
}
// Process payment
const paymentSpan = tracer.startSpan('processPayment')
const paymentResult = await this.paymentServiceClient.processPayment({
orderId: orderRequest.orderId,
amount: orderRequest.totalAmount,
userId: orderRequest.userId,
})
paymentSpan.end()
// Create order
const orderSpan = tracer.startSpan('createOrderInDB')
const order = await this.orderRepository.create(orderRequest)
orderSpan.end()
span.setStatus({ code: 1 }) // OK
return order
} catch (error) {
span.setStatus({ code: 2, message: error.message }) // ERROR
throw error
} finally {
span.end()
}
})
}
}

9. Centralized Loggingh3

Aggregate logs from all services:

// Structured logging with correlation IDs
class Logger {
private correlationId: string
constructor(correlationId?: string) {
this.correlationId = correlationId || uuidv4()
}
info(message: string, context?: any): void {
this.log('INFO', message, context)
}
error(message: string, error?: Error, context?: any): void {
this.log('ERROR', message, { ...context, error: error?.message, stack: error?.stack })
}
private log(level: string, message: string, context?: any): void {
const logEntry = {
timestamp: new Date().toISOString(),
level,
message,
correlationId: this.correlationId,
service: 'order-service',
version: process.env.npm_package_version,
...context,
}
// Send to centralized logging service
console.log(JSON.stringify(logEntry))
}
}
// Usage in service methods
class OrderService {
async createOrder(orderRequest: CreateOrderRequest): Promise<Order> {
const logger = new Logger(orderRequest.correlationId)
logger.info('Creating order', { userId: orderRequest.userId })
try {
const order = await this.orderRepository.create(orderRequest)
logger.info('Order created successfully', { orderId: order.id })
return order
} catch (error) {
logger.error('Failed to create order', error, { orderData: orderRequest })
throw error
}
}
}

Deployment Patternsh2

10. Container Orchestrationh3

Deploy microservices with Kubernetes:

order-service-deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
labels:
app: order-service
spec:
replicas: 3
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: order-service
image: myregistry/order-service:latest
ports:
- containerPort: 3000
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: order-service-secrets
key: database-url
- name: REDIS_URL
valueFrom:
secretKeyRef:
name: order-service-secrets
key: redis-url
- name: USER_SERVICE_URL
valueFrom:
configMapKeyRef:
name: service-config
key: user-service-url
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
resources:
requests:
memory: '128Mi'
cpu: '100m'
limits:
memory: '512Mi'
cpu: '500m'
---
# order-service-service.yml
apiVersion: v1
kind: Service
metadata:
name: order-service
spec:
selector:
app: order-service
ports:
- name: http
port: 80
targetPort: 3000
type: ClusterIP
---
# order-service-configmap.yml
apiVersion: v1
kind: ConfigMap
metadata:
name: service-config
data:
user-service-url: 'http://user-service:80'
payment-service-url: 'http://payment-service:80'
inventory-service-url: 'http://inventory-service:80'

Security Patternsh2

11. Service-to-Service Authenticationh3

Secure inter-service communication:

// JWT for service authentication
class ServiceAuthenticator {
private serviceToken: string
constructor() {
this.serviceToken = jwt.sign(
{
service: 'order-service',
permissions: ['read:users', 'write:orders'],
},
process.env.SERVICE_SECRET!,
{ expiresIn: '1h' }
)
}
getAuthHeaders(): Record<string, string> {
return {
'Authorization': `Bearer ${this.serviceToken}`,
'X-Service-Name': 'order-service',
'X-Request-ID': uuidv4(),
}
}
}
// API client with authentication
class UserServiceClient {
private baseURL: string
private authenticator: ServiceAuthenticator
constructor(baseURL: string) {
this.baseURL = baseURL
this.authenticator = new ServiceAuthenticator()
}
async getUser(userId: string): Promise<User> {
const response = await fetch(`${this.baseURL}/users/${userId}`, {
headers: {
'Content-Type': 'application/json',
...this.authenticator.getAuthHeaders(),
},
})
if (!response.ok) {
throw new Error(`User service error: ${response.status}`)
}
return response.json()
}
}

Monitoring and Alertingh2

12. Health Checks and Metricsh3

Monitor service health:

// Health check endpoint
class HealthController {
constructor(
private database: Database,
private redis: RedisClient,
private userServiceClient: UserServiceClient
) {}
async checkHealth(): Promise<HealthStatus> {
const checks = await Promise.allSettled([
this.checkDatabase(),
this.checkRedis(),
this.checkUserService(),
this.checkMemoryUsage(),
this.checkDiskSpace(),
])
const results = checks.map((check) => (check.status === 'fulfilled' ? check.value : { status: 'error', error: check.reason }))
const overallStatus = results.every((r) => r.status === 'ok') ? 'healthy' : 'unhealthy'
return {
status: overallStatus,
timestamp: new Date().toISOString(),
checks: results,
version: process.env.npm_package_version,
}
}
private async checkDatabase(): Promise<HealthCheck> {
try {
await this.database.query('SELECT 1')
return { name: 'database', status: 'ok' }
} catch (error) {
return { name: 'database', status: 'error', error: error.message }
}
}
private async checkRedis(): Promise<HealthCheck> {
try {
await this.redis.ping()
return { name: 'redis', status: 'ok' }
} catch (error) {
return { name: 'redis', status: 'error', error: error.message }
}
}
private async checkUserService(): Promise<HealthCheck> {
try {
await this.userServiceClient.getUser('test')
return { name: 'user-service', status: 'ok' }
} catch (error) {
return { name: 'user-service', status: 'error', error: error.message }
}
}
}
// Metrics collection
class MetricsCollector {
private metrics = {
requests_total: new Counter({
name: 'requests_total',
help: 'Total number of requests',
labelNames: ['method', 'endpoint', 'status_code'],
}),
request_duration_seconds: new Histogram({
name: 'request_duration_seconds',
help: 'Request duration in seconds',
labelNames: ['method', 'endpoint'],
buckets: [0.1, 0.5, 1, 2.5, 5, 10],
}),
active_connections: new Gauge({
name: 'active_connections',
help: 'Number of active connections',
}),
}
recordRequest(method: string, endpoint: string, statusCode: number, duration: number): void {
this.metrics.requests_total.inc({ method, endpoint, status_code: statusCode })
this.metrics.request_duration_seconds.observe({ method, endpoint }, duration)
}
updateActiveConnections(count: number): void {
this.metrics.active_connections.set(count)
}
}

Conclusionh2

Microservices architecture offers tremendous benefits but requires careful planning and implementation. The patterns I’ve shared here provide a solid foundation for building scalable, maintainable distributed systems.

Key takeaways:

  • Domain-driven design for service boundaries
  • API Gateway for request routing and security
  • Event-driven architecture for loose coupling
  • Database per service for data ownership
  • Observability for monitoring and debugging
  • Security for inter-service communication

Start small, iterate, and continuously improve your microservices architecture. The investment in proper design pays dividends in scalability and maintainability.

What microservices challenges are you facing? Which patterns have worked best for your distributed systems? Share your experiences!

Further Readingh2


This post reflects my experience as of October 2025. Microservices technologies and best practices evolve rapidly, so always evaluate the latest solutions for your specific needs.