DevDocsDev Docs
Software Architecture

Microservices Architecture

Build distributed systems with independent, loosely coupled services

Microservices Architecture

Microservices architecture structures an application as a collection of small, autonomous services that are independently deployable, scalable, and maintainable. Each service owns its data and communicates through well-defined APIs.

Core Characteristics

Single Responsibility

Each service focuses on one business capability and does it well.

Independent Deployment

Services can be deployed without affecting others.

Decentralized Data

Each service owns and manages its own database.

Technology Agnostic

Services can use different languages, frameworks, and databases.

Resilience

Failure in one service doesn't bring down the entire system.

Scalability

Services can be scaled independently based on demand.

Service Design Patterns

API Gateway Pattern

Single entry point for all clients, handling routing, composition, and cross-cutting concerns.

gateway.ts
// API Gateway with Hono
import { Hono } from 'hono';
import { jwt } from 'hono/jwt';
import { rateLimiter } from 'hono-rate-limiter';

const gateway = new Hono();

// Cross-cutting concerns
gateway.use('*', rateLimiter({ windowMs: 60000, max: 100 }));
gateway.use('/api/*', jwt({ secret: process.env.JWT_SECRET! }));

// Route to services
gateway.all('/api/users/*', async (c) => {
  const path = c.req.path.replace('/api/users', '');
  return fetch(`${USER_SERVICE_URL}${path}`, {
    method: c.req.method,
    headers: c.req.raw.headers,
    body: c.req.raw.body
  });
});

gateway.all('/api/orders/*', async (c) => {
  const path = c.req.path.replace('/api/orders', '');
  return fetch(`${ORDER_SERVICE_URL}${path}`, {
    method: c.req.method,
    headers: c.req.raw.headers,
    body: c.req.raw.body
  });
});

// Aggregation endpoint
gateway.get('/api/dashboard', async (c) => {
  const userId = c.get('jwtPayload').sub;
  
  const [user, orders, recommendations] = await Promise.all([
    fetch(`${USER_SERVICE_URL}/users/${userId}`).then(r => r.json()),
    fetch(`${ORDER_SERVICE_URL}/users/${userId}/orders`).then(r => r.json()),
    fetch(`${PRODUCT_SERVICE_URL}/recommendations/${userId}`).then(r => r.json())
  ]);
  
  return c.json({ user, orders, recommendations });
});

Service Discovery

Automatically detect and locate services in a distributed system.

service-registry.ts
// Simple service registry
type ServiceInstance = {
  id: string;
  name: string;
  host: string;
  port: number;
  health: 'healthy' | 'unhealthy';
  lastHeartbeat: Date;
};

const serviceRegistry = {
  instances: new Map<string, ServiceInstance[]>(),
  
  register: (instance: Omit<ServiceInstance, 'lastHeartbeat'>) => {
    const existing = serviceRegistry.instances.get(instance.name) || [];
    const updated = existing.filter(i => i.id !== instance.id);
    updated.push({ ...instance, lastHeartbeat: new Date() });
    serviceRegistry.instances.set(instance.name, updated);
  },
  
  deregister: (serviceName: string, instanceId: string) => {
    const existing = serviceRegistry.instances.get(serviceName) || [];
    serviceRegistry.instances.set(
      serviceName,
      existing.filter(i => i.id !== instanceId)
    );
  },
  
  discover: (serviceName: string): ServiceInstance | null => {
    const instances = serviceRegistry.instances.get(serviceName) || [];
    const healthy = instances.filter(i => i.health === 'healthy');
    if (healthy.length === 0) return null;
    // Round-robin load balancing
    return healthy[Math.floor(Math.random() * healthy.length)];
  },
  
  heartbeat: (serviceName: string, instanceId: string) => {
    const instances = serviceRegistry.instances.get(serviceName) || [];
    const instance = instances.find(i => i.id === instanceId);
    if (instance) instance.lastHeartbeat = new Date();
  }
};

// Service client with discovery
const createServiceClient = (serviceName: string) => ({
  call: async <T>(path: string, options?: RequestInit): Promise<T> => {
    const instance = serviceRegistry.discover(serviceName);
    if (!instance) throw new Error(`No healthy instances of ${serviceName}`);
    
    const url = `http://${instance.host}:${instance.port}${path}`;
    const response = await fetch(url, options);
    return response.json();
  }
});

// Usage
const userService = createServiceClient('user-service');
const user = await userService.call<User>('/users/123');

Circuit Breaker Pattern

Prevent cascading failures by failing fast when a service is unhealthy.

circuit-breaker.ts
type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN';

type CircuitBreakerOptions = {
  failureThreshold: number;
  successThreshold: number;
  timeout: number;
};

const createCircuitBreaker = <T>(
  fn: () => Promise<T>,
  options: CircuitBreakerOptions
) => {
  let state: CircuitState = 'CLOSED';
  let failures = 0;
  let successes = 0;
  let lastFailureTime: number | null = null;
  
  const reset = () => {
    failures = 0;
    successes = 0;
    state = 'CLOSED';
  };
  
  const recordSuccess = () => {
    failures = 0;
    if (state === 'HALF_OPEN') {
      successes++;
      if (successes >= options.successThreshold) {
        reset();
      }
    }
  };
  
  const recordFailure = () => {
    failures++;
    lastFailureTime = Date.now();
    if (failures >= options.failureThreshold) {
      state = 'OPEN';
    }
  };
  
  return async (): Promise<T> => {
    if (state === 'OPEN') {
      if (Date.now() - (lastFailureTime || 0) >= options.timeout) {
        state = 'HALF_OPEN';
        successes = 0;
      } else {
        throw new Error('Circuit breaker is OPEN');
      }
    }
    
    try {
      const result = await fn();
      recordSuccess();
      return result;
    } catch (error) {
      recordFailure();
      throw error;
    }
  };
};

// Usage
const fetchUserWithBreaker = createCircuitBreaker(
  () => fetch('http://user-service/users/123').then(r => r.json()),
  { failureThreshold: 5, successThreshold: 2, timeout: 30000 }
);

try {
  const user = await fetchUserWithBreaker();
} catch (error) {
  // Handle fallback
  return getCachedUser('123');
}

Saga Pattern

Manage distributed transactions across multiple services.

saga.ts
// Saga orchestrator
type SagaStep<T> = {
  name: string;
  execute: (context: T) => Promise<T>;
  compensate: (context: T) => Promise<T>;
};

const createSaga = <T>(steps: SagaStep<T>[]) => ({
  execute: async (initialContext: T): Promise<T> => {
    const executedSteps: SagaStep<T>[] = [];
    let context = initialContext;
    
    try {
      for (const step of steps) {
        console.log(`Executing: ${step.name}`);
        context = await step.execute(context);
        executedSteps.push(step);
      }
      return context;
    } catch (error) {
      console.error('Saga failed, compensating...');
      
      // Compensate in reverse order
      for (const step of executedSteps.reverse()) {
        try {
          console.log(`Compensating: ${step.name}`);
          context = await step.compensate(context);
        } catch (compensateError) {
          console.error(`Compensation failed for ${step.name}`, compensateError);
        }
      }
      
      throw error;
    }
  }
});

// Order saga example
type OrderContext = {
  orderId: string;
  userId: string;
  items: { productId: string; quantity: number }[];
  reservationId?: string;
  paymentId?: string;
};

const orderSaga = createSaga<OrderContext>([
  {
    name: 'Reserve Inventory',
    execute: async (ctx) => {
      const reservation = await inventoryService.reserve(ctx.items);
      return { ...ctx, reservationId: reservation.id };
    },
    compensate: async (ctx) => {
      if (ctx.reservationId) {
        await inventoryService.release(ctx.reservationId);
      }
      return ctx;
    }
  },
  {
    name: 'Process Payment',
    execute: async (ctx) => {
      const payment = await paymentService.charge(ctx.userId, ctx.orderId);
      return { ...ctx, paymentId: payment.id };
    },
    compensate: async (ctx) => {
      if (ctx.paymentId) {
        await paymentService.refund(ctx.paymentId);
      }
      return ctx;
    }
  },
  {
    name: 'Confirm Order',
    execute: async (ctx) => {
      await orderService.confirm(ctx.orderId);
      await notificationService.sendOrderConfirmation(ctx.userId, ctx.orderId);
      return ctx;
    },
    compensate: async (ctx) => {
      await orderService.cancel(ctx.orderId);
      return ctx;
    }
  }
]);

// Usage
await orderSaga.execute({
  orderId: 'order-123',
  userId: 'user-456',
  items: [{ productId: 'prod-1', quantity: 2 }]
});

Communication Patterns

REST / gRPC Communication

Direct request-response communication between services.

rest-client.ts
// REST client with retry and timeout
const createRestClient = (baseUrl: string) => {
  const fetchWithRetry = async (
    path: string,
    options: RequestInit = {},
    retries = 3
  ): Promise<Response> => {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), 5000);
    
    try {
      const response = await fetch(`${baseUrl}${path}`, {
        ...options,
        signal: controller.signal,
        headers: {
          'Content-Type': 'application/json',
          ...options.headers
        }
      });
      
      if (!response.ok && retries > 0) {
        return fetchWithRetry(path, options, retries - 1);
      }
      
      return response;
    } catch (error) {
      if (retries > 0) {
        await new Promise(r => setTimeout(r, 1000));
        return fetchWithRetry(path, options, retries - 1);
      }
      throw error;
    } finally {
      clearTimeout(timeout);
    }
  };
  
  return {
    get: <T>(path: string) => 
      fetchWithRetry(path).then(r => r.json() as Promise<T>),
    post: <T>(path: string, body: unknown) =>
      fetchWithRetry(path, {
        method: 'POST',
        body: JSON.stringify(body)
      }).then(r => r.json() as Promise<T>),
    put: <T>(path: string, body: unknown) =>
      fetchWithRetry(path, {
        method: 'PUT',
        body: JSON.stringify(body)
      }).then(r => r.json() as Promise<T>),
    delete: (path: string) =>
      fetchWithRetry(path, { method: 'DELETE' })
  };
};

// Usage
const userClient = createRestClient('http://user-service:3000');
const user = await userClient.get<User>('/users/123');

Message Queue Communication

Decouple services using message brokers like RabbitMQ or SQS.

message-broker.ts
// Message broker abstraction
type Message<T> = {
  id: string;
  type: string;
  payload: T;
  timestamp: Date;
  correlationId?: string;
};

type MessageHandler<T> = (message: Message<T>) => Promise<void>;

const createMessageBroker = () => {
  const handlers = new Map<string, MessageHandler<unknown>[]>();
  
  return {
    publish: async <T>(type: string, payload: T, correlationId?: string) => {
      const message: Message<T> = {
        id: crypto.randomUUID(),
        type,
        payload,
        timestamp: new Date(),
        correlationId
      };
      
      // In production, send to RabbitMQ/SQS
      const typeHandlers = handlers.get(type) || [];
      await Promise.all(
        typeHandlers.map(handler => handler(message as Message<unknown>))
      );
    },
    
    subscribe: <T>(type: string, handler: MessageHandler<T>) => {
      const existing = handlers.get(type) || [];
      handlers.set(type, [...existing, handler as MessageHandler<unknown>]);
      
      return () => {
        const current = handlers.get(type) || [];
        handlers.set(type, current.filter(h => h !== handler));
      };
    }
  };
};

// Usage
const broker = createMessageBroker();

// Publisher (Order Service)
await broker.publish('order.created', {
  orderId: '123',
  userId: 'user-456',
  items: [{ productId: 'prod-1', quantity: 2 }]
});

// Subscriber (Email Service)
broker.subscribe<OrderCreatedPayload>('order.created', async (message) => {
  await sendOrderConfirmationEmail(message.payload.userId, message.payload.orderId);
});

// Subscriber (Inventory Service)
broker.subscribe<OrderCreatedPayload>('order.created', async (message) => {
  await updateInventory(message.payload.items);
});

Event Sourcing

Store state changes as a sequence of events.

event-store.ts
// Event store
type Event<T = unknown> = {
  id: string;
  aggregateId: string;
  type: string;
  payload: T;
  version: number;
  timestamp: Date;
};

const createEventStore = () => {
  const events: Event[] = [];
  
  return {
    append: async (event: Omit<Event, 'id' | 'timestamp'>) => {
      const newEvent: Event = {
        ...event,
        id: crypto.randomUUID(),
        timestamp: new Date()
      };
      events.push(newEvent);
      return newEvent;
    },
    
    getEvents: (aggregateId: string, fromVersion = 0) => {
      return events
        .filter(e => e.aggregateId === aggregateId && e.version > fromVersion)
        .sort((a, b) => a.version - b.version);
    },
    
    getAllEvents: (fromTimestamp?: Date) => {
      return events
        .filter(e => !fromTimestamp || e.timestamp > fromTimestamp)
        .sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
    }
  };
};

// Aggregate that rebuilds state from events
type OrderState = {
  id: string;
  status: 'pending' | 'confirmed' | 'shipped' | 'cancelled';
  items: { productId: string; quantity: number }[];
  total: number;
};

const rebuildOrder = (events: Event[]): OrderState | null => {
  return events.reduce<OrderState | null>((state, event) => {
    switch (event.type) {
      case 'OrderCreated':
        return {
          id: event.aggregateId,
          status: 'pending',
          items: event.payload.items,
          total: event.payload.total
        };
      case 'OrderConfirmed':
        return state ? { ...state, status: 'confirmed' } : null;
      case 'OrderShipped':
        return state ? { ...state, status: 'shipped' } : null;
      case 'OrderCancelled':
        return state ? { ...state, status: 'cancelled' } : null;
      default:
        return state;
    }
  }, null);
};

Service Structure

package.json
Dockerfile
docker-compose.yml
index.ts
app.ts

Data Management

Each microservice should own its data. Never share databases between services!

Data Consistency Strategies

StrategyUse CaseConsistencyComplexity
SagaDistributed transactionsEventualHigh
Event SourcingAudit trails, replayEventualHigh
Two-Phase CommitStrong consistency neededStrongVery High
CQRSHigh read/write ratioEventualMedium

Deployment Patterns

user-service-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service
  labels:
    app: user-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: user-service
  template:
    metadata:
      labels:
        app: user-service
    spec:
      containers:
      - name: user-service
        image: myregistry/user-service:latest
        ports:
        - containerPort: 3000
        env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: user-service-secrets
              key: database-url
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "256Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 10
          periodSeconds: 5
        readinessProbe:
          httpGet:
            path: /ready
            port: 3000
          initialDelaySeconds: 5
          periodSeconds: 3
---
apiVersion: v1
kind: Service
metadata:
  name: user-service
spec:
  selector:
    app: user-service
  ports:
  - port: 80
    targetPort: 3000
  type: ClusterIP
docker-compose.yml
version: '3.8'

services:
  api-gateway:
    build: ./gateway
    ports:
      - "8080:8080"
    depends_on:
      - user-service
      - order-service
      - product-service
    environment:
      - USER_SERVICE_URL=http://user-service:3000
      - ORDER_SERVICE_URL=http://order-service:3001
      - PRODUCT_SERVICE_URL=http://product-service:3002

  user-service:
    build: ./services/user
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgresql://postgres:password@user-db:5432/users
      - REDIS_URL=redis://redis:6379
    depends_on:
      - user-db
      - redis

  order-service:
    build: ./services/order
    ports:
      - "3001:3001"
    environment:
      - DATABASE_URL=postgresql://postgres:password@order-db:5432/orders
      - RABBITMQ_URL=amqp://rabbitmq:5672
    depends_on:
      - order-db
      - rabbitmq

  product-service:
    build: ./services/product
    ports:
      - "3002:3002"
    environment:
      - DATABASE_URL=postgresql://postgres:password@product-db:5432/products

  user-db:
    image: postgres:15-alpine
    environment:
      - POSTGRES_DB=users
      - POSTGRES_PASSWORD=password
    volumes:
      - user-data:/var/lib/postgresql/data

  order-db:
    image: postgres:15-alpine
    environment:
      - POSTGRES_DB=orders
      - POSTGRES_PASSWORD=password
    volumes:
      - order-data:/var/lib/postgresql/data

  product-db:
    image: postgres:15-alpine
    environment:
      - POSTGRES_DB=products
      - POSTGRES_PASSWORD=password
    volumes:
      - product-data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine

  rabbitmq:
    image: rabbitmq:3-management-alpine
    ports:
      - "15672:15672"

volumes:
  user-data:
  order-data:
  product-data:
sst.config.ts
// AWS Lambda microservice deployment with SST
export default {
  config() {
    return { name: "microservices", region: "us-east-1" };
  },
  stacks(app) {
    app.stack(function API({ stack }) {
      // User Service
      const userService = new Function(stack, "UserService", {
        handler: "services/user/handler.main",
        environment: {
          TABLE_NAME: userTable.tableName,
        },
      });
      
      // Order Service
      const orderService = new Function(stack, "OrderService", {
        handler: "services/order/handler.main",
        environment: {
          TABLE_NAME: orderTable.tableName,
          USER_SERVICE_URL: userApi.url,
        },
      });
      
      // API Gateway
      const api = new Api(stack, "Api", {
        routes: {
          "GET /users/{id}": userService,
          "POST /users": userService,
          "GET /orders/{id}": orderService,
          "POST /orders": orderService,
        },
      });
      
      return { api };
    });
  },
};

Observability

The Three Pillars

tracing.ts
// Distributed tracing
const createTracer = () => {
  return {
    startSpan: (name: string, parentId?: string) => {
      const spanId = crypto.randomUUID();
      const traceId = parentId ? parentId.split(':')[0] : crypto.randomUUID();
      
      return {
        id: `${traceId}:${spanId}`,
        name,
        startTime: Date.now(),
        end: (status: 'ok' | 'error' = 'ok') => {
          console.log(JSON.stringify({
            traceId,
            spanId,
            parentId,
            name,
            duration: Date.now() - Date.now(),
            status
          }));
        }
      };
    }
  };
};

// Usage in service
const tracer = createTracer();

app.use('*', async (c, next) => {
  const parentTraceId = c.req.header('x-trace-id');
  const span = tracer.startSpan('http-request', parentTraceId);
  
  c.set('traceId', span.id);
  c.header('x-trace-id', span.id);
  
  try {
    await next();
    span.end('ok');
  } catch (error) {
    span.end('error');
    throw error;
  }
});

When to Use Microservices

✅ Good For

  • Large teams (20+ developers)
  • Complex domains - Multiple bounded contexts
  • Independent scaling - Different resource needs
  • Technology diversity - Best tool for each job
  • Frequent deployments - Reduce blast radius
  • High availability - Fault isolation

❌ Avoid When

  • Small teams - Operational overhead
  • Unclear boundaries - Leads to distributed monolith
  • Tight coupling - Constant cross-service changes
  • Limited DevOps - Complex infrastructure
  • Early-stage products - Requirements still evolving

Summary

Microservices offer tremendous benefits for the right use cases but come with significant complexity. Key takeaways:

  1. Start with clear boundaries - Use Domain-Driven Design
  2. Embrace eventual consistency - Not everything needs ACID
  3. Invest in infrastructure - CI/CD, monitoring, service mesh
  4. Design for failure - Circuit breakers, retries, fallbacks
  5. Consider starting with a modular monolith - Extract services as needed

On this page