Microservices Architecture: From Monolith to Distributed Systems

Master microservices core patterns, service decomposition strategies, and distributed system best practices

Microservices Architecture: From Monolith to Distributed Systems

Microservices architecture has become the mainstream approach for building large-scale distributed systems. This article explores the core concepts, design patterns, and practical experience with microservices.

Architecture Evolution

From Monolith to Microservices

Architecture Evolution Path:
┌─────────────────────────────────────────────────────┐
│                                                     │
│   Monolith                                          │
│   ├── All functionality in one application         │
│   ├── Simple deployment                            │
│   └── Difficult to maintain as scale grows         │
│              │                                      │
│              ▼                                      │
│   Modular Monolith                                  │
│   ├── Internal modularity                          │
│   ├── Clear boundaries                             │
│   └── Single deployment unit                       │
│              │                                      │
│              ▼                                      │
│   Microservices                                     │
│   ├── Independently deployable services            │
│   ├── Technology diversity                         │
│   └── Distributed complexity                       │
│                                                     │
└─────────────────────────────────────────────────────┘

When to Adopt Microservices

ScenarioMonolithMicroservices
Team Size< 10 people> 10 people
Codebase Size< 100K LOC> 100K LOC
Deploy FrequencyWeekly/MonthlyDaily/Hourly
Scaling NeedsScale everythingScale as needed
Tech StackUnifiedDiverse

Core Design Principles

Single Responsibility

Service Responsibility Division:
┌─────────────────────────────────────────────────────┐
│                                                     │
│   ✅ Good Division                                   │
│   ┌─────────┐  ┌─────────┐  ┌─────────┐            │
│   │  User   │  │  Order  │  │ Payment │            │
│   │ Service │  │ Service │  │ Service │            │
│   │         │  │         │  │         │            │
│   │ • Signup│  │ • Create│  │ • Process│           │
│   │ • Auth  │  │ • Query │  │ • Refund │           │
│   │ • Profile│ │ • Status│  │ • Reconcile│         │
│   └─────────┘  └─────────┘  └─────────┘            │
│                                                     │
│   ❌ Mixed Responsibilities                          │
│   ┌─────────────────────────────────────────┐      │
│   │         Business Service                  │      │
│   │  User + Order + Payment + Inventory...   │      │
│   └─────────────────────────────────────────┘      │
│                                                     │
└─────────────────────────────────────────────────────┘

Domain-Driven Design (DDD)

// Aggregate Root definition
interface Order {
  id: string;
  customerId: string;
  items: OrderItem[];
  status: OrderStatus;
  totalAmount: Money;
  createdAt: Date;

  // Domain behaviors
  addItem(product: Product, quantity: number): void;
  removeItem(itemId: string): void;
  confirm(): void;
  cancel(): void;
}

// Value Object
interface Money {
  amount: number;
  currency: string;
}

// Domain Event
interface OrderCreatedEvent {
  orderId: string;
  customerId: string;
  items: OrderItem[];
  totalAmount: Money;
  occurredAt: Date;
}

// Repository Interface
interface OrderRepository {
  save(order: Order): Promise<void>;
  findById(id: string): Promise<Order | null>;
  findByCustomerId(customerId: string): Promise<Order[]>;
}

Bounded Contexts

E-commerce Bounded Contexts:
┌─────────────────────────────────────────────────────┐
│                                                     │
│  ┌──────────────────┐   ┌──────────────────┐       │
│  │   Sales Context   │   │ Inventory Context│       │
│  │                  │   │                  │       │
│  │  Product         │   │  StockItem       │       │
│  │  • name          │   │  • sku           │       │
│  │  • price         │   │  • quantity      │       │
│  │  • description   │   │  • location      │       │
│  │                  │   │                  │       │
│  └──────────────────┘   └──────────────────┘       │
│           │                      │                  │
│           └──────────┬───────────┘                  │
│                      │                              │
│            ┌─────────▼─────────┐                   │
│            │ Anti-Corruption   │                   │
│            │    Layer (ACL)    │                   │
│            │ ProductId ↔ SKU   │                   │
│            └───────────────────┘                   │
│                                                     │
└─────────────────────────────────────────────────────┘

Service Communication

Synchronous Communication

// REST API
// user-service/src/controllers/user.controller.ts
import express from 'express';

const router = express.Router();

router.get('/users/:id', async (req, res) => {
  const user = await userService.findById(req.params.id);
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  res.json(user);
});

router.post('/users', async (req, res) => {
  const user = await userService.create(req.body);
  res.status(201).json(user);
});

// gRPC Service Definition
// proto/user.proto
syntax = "proto3";

service UserService {
  rpc GetUser(GetUserRequest) returns (User);
  rpc CreateUser(CreateUserRequest) returns (User);
  rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
}

message User {
  string id = 1;
  string name = 2;
  string email = 3;
  string created_at = 4;
}

message GetUserRequest {
  string id = 1;
}

Asynchronous Communication

// Event Publisher
// order-service/src/events/publisher.ts
import { Kafka, Producer } from 'kafkajs';

class EventPublisher {
  private producer: Producer;

  constructor(kafka: Kafka) {
    this.producer = kafka.producer();
  }

  async publish(topic: string, event: DomainEvent): Promise<void> {
    await this.producer.send({
      topic,
      messages: [
        {
          key: event.aggregateId,
          value: JSON.stringify(event),
          headers: {
            eventType: event.type,
            timestamp: event.occurredAt.toISOString(),
          },
        },
      ],
    });
  }
}

// Publish event after order creation
async function createOrder(data: CreateOrderDto): Promise<Order> {
  const order = Order.create(data);
  await orderRepository.save(order);

  await eventPublisher.publish('order-events', {
    type: 'OrderCreated',
    aggregateId: order.id,
    payload: {
      orderId: order.id,
      customerId: order.customerId,
      items: order.items,
      totalAmount: order.totalAmount,
    },
    occurredAt: new Date(),
  });

  return order;
}

// Event Consumer
// inventory-service/src/events/consumer.ts
import { Kafka, Consumer } from 'kafkajs';

class OrderEventConsumer {
  private consumer: Consumer;

  async start(): Promise<void> {
    await this.consumer.subscribe({ topic: 'order-events' });

    await this.consumer.run({
      eachMessage: async ({ message }) => {
        const event = JSON.parse(message.value!.toString());

        switch (event.type) {
          case 'OrderCreated':
            await this.handleOrderCreated(event.payload);
            break;
          case 'OrderCancelled':
            await this.handleOrderCancelled(event.payload);
            break;
        }
      },
    });
  }

  private async handleOrderCreated(payload: OrderCreatedPayload): Promise<void> {
    // Reserve inventory
    for (const item of payload.items) {
      await inventoryService.reserve(item.productId, item.quantity);
    }
  }
}

Distributed Patterns

API Gateway

// gateway/src/index.ts
import express from 'express';
import { createProxyMiddleware } from 'http-proxy-middleware';
import rateLimit from 'express-rate-limit';
import jwt from 'jsonwebtoken';

const app = express();

// Rate limiting
const limiter = rateLimit({
  windowMs: 60 * 1000,
  max: 100,
  message: { error: 'Too many requests' },
});

app.use(limiter);

// JWT authentication middleware
const authMiddleware = (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) {
    return res.status(401).json({ error: 'Unauthorized' });
  }

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET!);
    req.user = decoded;
    next();
  } catch {
    res.status(401).json({ error: 'Invalid token' });
  }
};

// Service routing
app.use('/api/users', authMiddleware, createProxyMiddleware({
  target: process.env.USER_SERVICE_URL,
  changeOrigin: true,
  pathRewrite: { '^/api/users': '' },
}));

app.use('/api/orders', authMiddleware, createProxyMiddleware({
  target: process.env.ORDER_SERVICE_URL,
  changeOrigin: true,
  pathRewrite: { '^/api/orders': '' },
}));

app.use('/api/products', createProxyMiddleware({
  target: process.env.PRODUCT_SERVICE_URL,
  changeOrigin: true,
  pathRewrite: { '^/api/products': '' },
}));

Service Discovery

# Kubernetes Service Discovery
apiVersion: v1
kind: Service
metadata:
  name: user-service
  namespace: production
spec:
  selector:
    app: user-service
  ports:
    - port: 80
      targetPort: 8080

---
# Application uses DNS name
# user-service.production.svc.cluster.local
// Consul Service Registration
import Consul from 'consul';

const consul = new Consul({ host: 'consul.local' });

async function registerService(): Promise<void> {
  await consul.agent.service.register({
    name: 'user-service',
    id: `user-service-${process.env.HOSTNAME}`,
    address: process.env.POD_IP,
    port: 8080,
    check: {
      http: `http://${process.env.POD_IP}:8080/health`,
      interval: '10s',
    },
  });
}

async function discoverService(name: string): Promise<string[]> {
  const services = await consul.catalog.service.nodes(name);
  return services.map(s => `http://${s.ServiceAddress}:${s.ServicePort}`);
}

Circuit Breaker Pattern

// circuit-breaker.ts
import CircuitBreaker from 'opossum';

interface CircuitBreakerOptions {
  timeout: number;
  errorThresholdPercentage: number;
  resetTimeout: number;
}

function createCircuitBreaker<T>(
  fn: (...args: any[]) => Promise<T>,
  options: CircuitBreakerOptions
): CircuitBreaker {
  const breaker = new CircuitBreaker(fn, {
    timeout: options.timeout,
    errorThresholdPercentage: options.errorThresholdPercentage,
    resetTimeout: options.resetTimeout,
  });

  breaker.on('open', () => {
    console.log('Circuit breaker opened');
  });

  breaker.on('halfOpen', () => {
    console.log('Circuit breaker half-open');
  });

  breaker.on('close', () => {
    console.log('Circuit breaker closed');
  });

  breaker.fallback(() => {
    return { error: 'Service temporarily unavailable' };
  });

  return breaker;
}

// Usage example
const userServiceBreaker = createCircuitBreaker(
  (userId: string) => fetch(`http://user-service/users/${userId}`).then(r => r.json()),
  {
    timeout: 3000,
    errorThresholdPercentage: 50,
    resetTimeout: 30000,
  }
);

async function getUser(userId: string) {
  return userServiceBreaker.fire(userId);
}

Saga Pattern

// saga/order-saga.ts
interface SagaStep<T> {
  name: string;
  execute: (context: T) => Promise<void>;
  compensate: (context: T) => Promise<void>;
}

class OrderSaga {
  private steps: SagaStep<OrderContext>[] = [
    {
      name: 'reserveInventory',
      execute: async (ctx) => {
        ctx.inventoryReservationId = await inventoryService.reserve(
          ctx.items
        );
      },
      compensate: async (ctx) => {
        if (ctx.inventoryReservationId) {
          await inventoryService.cancelReservation(ctx.inventoryReservationId);
        }
      },
    },
    {
      name: 'processPayment',
      execute: async (ctx) => {
        ctx.paymentId = await paymentService.process({
          orderId: ctx.orderId,
          amount: ctx.totalAmount,
          customerId: ctx.customerId,
        });
      },
      compensate: async (ctx) => {
        if (ctx.paymentId) {
          await paymentService.refund(ctx.paymentId);
        }
      },
    },
    {
      name: 'createShipment',
      execute: async (ctx) => {
        ctx.shipmentId = await shippingService.createShipment({
          orderId: ctx.orderId,
          items: ctx.items,
          address: ctx.shippingAddress,
        });
      },
      compensate: async (ctx) => {
        if (ctx.shipmentId) {
          await shippingService.cancelShipment(ctx.shipmentId);
        }
      },
    },
  ];

  async execute(context: OrderContext): Promise<void> {
    const executedSteps: SagaStep<OrderContext>[] = [];

    try {
      for (const step of this.steps) {
        console.log(`Executing step: ${step.name}`);
        await step.execute(context);
        executedSteps.push(step);
      }
    } catch (error) {
      console.error('Saga failed, compensating...');

      // Compensate in reverse order
      for (const step of executedSteps.reverse()) {
        try {
          console.log(`Compensating step: ${step.name}`);
          await step.compensate(context);
        } catch (compensateError) {
          console.error(`Compensation failed for ${step.name}`, compensateError);
          // Log failure for manual intervention
        }
      }

      throw error;
    }
  }
}

Data Management

Database per Service

Database Isolation Strategy:
┌─────────────────────────────────────────────────────┐
│                                                     │
│   ┌──────────┐  ┌──────────┐  ┌──────────┐         │
│   │   User   │  │  Order   │  │ Inventory│         │
│   │ Service  │  │ Service  │  │ Service  │         │
│   └────┬─────┘  └────┬─────┘  └────┬─────┘         │
│        │             │             │                │
│   ┌────▼─────┐  ┌────▼─────┐  ┌────▼─────┐         │
│   │PostgreSQL│  │ MongoDB  │  │  Redis   │         │
│   │  Users   │  │  Orders  │  │ Inventory│         │
│   └──────────┘  └──────────┘  └──────────┘         │
│                                                     │
│   Benefits:                                         │
│   • Technology diversity                           │
│   • Independent scaling                            │
│   • Fault isolation                                │
│                                                     │
│   Challenges:                                       │
│   • Cross-service queries                          │
│   • Data consistency                               │
│   • Distributed transactions                       │
│                                                     │
└─────────────────────────────────────────────────────┘

CQRS Pattern

// Command side - Write model
interface CreateOrderCommand {
  customerId: string;
  items: { productId: string; quantity: number }[];
  shippingAddress: Address;
}

class OrderCommandHandler {
  async handle(command: CreateOrderCommand): Promise<string> {
    const order = Order.create(command);
    await this.orderRepository.save(order);

    // Publish event to update read model
    await this.eventBus.publish(new OrderCreatedEvent(order));

    return order.id;
  }
}

// Query side - Read model
interface OrderReadModel {
  id: string;
  customerName: string;
  customerEmail: string;
  items: {
    productName: string;
    quantity: number;
    price: number;
  }[];
  totalAmount: number;
  status: string;
  createdAt: Date;
}

class OrderQueryHandler {
  // Uses optimized read model database (e.g., Elasticsearch)
  private readStore: ReadModelStore;

  async getOrderDetails(orderId: string): Promise<OrderReadModel> {
    return this.readStore.findById(orderId);
  }

  async getOrdersByCustomer(customerId: string): Promise<OrderReadModel[]> {
    return this.readStore.findByCustomerId(customerId);
  }
}

// Projector - Updates read model
class OrderProjection {
  @EventHandler(OrderCreatedEvent)
  async onOrderCreated(event: OrderCreatedEvent): Promise<void> {
    const customer = await this.customerService.getById(event.customerId);
    const products = await this.productService.getByIds(
      event.items.map(i => i.productId)
    );

    const readModel: OrderReadModel = {
      id: event.orderId,
      customerName: customer.name,
      customerEmail: customer.email,
      items: event.items.map(item => ({
        productName: products.find(p => p.id === item.productId)!.name,
        quantity: item.quantity,
        price: item.price,
      })),
      totalAmount: event.totalAmount,
      status: 'created',
      createdAt: event.occurredAt,
    };

    await this.readStore.save(readModel);
  }
}

Event Sourcing

// Event Store
interface Event {
  id: string;
  aggregateId: string;
  aggregateType: string;
  type: string;
  payload: Record<string, any>;
  version: number;
  occurredAt: Date;
}

class EventStore {
  async append(event: Event): Promise<void> {
    await this.db.collection('events').insertOne(event);
  }

  async getEvents(aggregateId: string): Promise<Event[]> {
    return this.db
      .collection('events')
      .find({ aggregateId })
      .sort({ version: 1 })
      .toArray();
  }
}

// Aggregate Reconstruction
class Order {
  private id: string;
  private status: OrderStatus;
  private items: OrderItem[] = [];

  static async fromEvents(events: Event[]): Promise<Order> {
    const order = new Order();

    for (const event of events) {
      order.apply(event);
    }

    return order;
  }

  private apply(event: Event): void {
    switch (event.type) {
      case 'OrderCreated':
        this.id = event.payload.orderId;
        this.status = 'created';
        break;
      case 'ItemAdded':
        this.items.push(event.payload.item);
        break;
      case 'OrderConfirmed':
        this.status = 'confirmed';
        break;
      case 'OrderShipped':
        this.status = 'shipped';
        break;
    }
  }
}

Observability

Distributed Tracing

// OpenTelemetry Configuration
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';

const sdk = new NodeSDK({
  traceExporter: new OTLPTraceExporter({
    url: 'http://jaeger:4318/v1/traces',
  }),
  instrumentations: [
    new HttpInstrumentation(),
    new ExpressInstrumentation(),
  ],
});

sdk.start();

// Manual tracing
import { trace, SpanStatusCode } from '@opentelemetry/api';

const tracer = trace.getTracer('order-service');

async function processOrder(orderId: string): Promise<void> {
  const span = tracer.startSpan('processOrder');
  span.setAttribute('orderId', orderId);

  try {
    await validateOrder(orderId);
    span.addEvent('Order validated');

    await reserveInventory(orderId);
    span.addEvent('Inventory reserved');

    await processPayment(orderId);
    span.addEvent('Payment processed');

    span.setStatus({ code: SpanStatusCode.OK });
  } catch (error) {
    span.setStatus({
      code: SpanStatusCode.ERROR,
      message: error.message,
    });
    throw error;
  } finally {
    span.end();
  }
}

Log Aggregation

// Structured logging
import pino from 'pino';

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  formatters: {
    level: (label) => ({ level: label }),
  },
  base: {
    service: 'order-service',
    version: process.env.APP_VERSION,
    environment: process.env.NODE_ENV,
  },
});

// Request logging middleware
const requestLogger = (req, res, next) => {
  const startTime = Date.now();

  res.on('finish', () => {
    logger.info({
      method: req.method,
      path: req.path,
      statusCode: res.statusCode,
      duration: Date.now() - startTime,
      traceId: req.headers['x-trace-id'],
      userId: req.user?.id,
    });
  });

  next();
};

// Business logging
logger.info({ orderId, customerId }, 'Order created');
logger.error({ orderId, error: err.message }, 'Order processing failed');

Health Checks

// Health check endpoints
import express from 'express';

const app = express();

// Liveness check
app.get('/health/live', (req, res) => {
  res.json({ status: 'ok' });
});

// Readiness check
app.get('/health/ready', async (req, res) => {
  const checks = await Promise.allSettled([
    checkDatabase(),
    checkMessageQueue(),
    checkCache(),
  ]);

  const results = {
    database: checks[0].status === 'fulfilled' ? 'ok' : 'error',
    messageQueue: checks[1].status === 'fulfilled' ? 'ok' : 'error',
    cache: checks[2].status === 'fulfilled' ? 'ok' : 'error',
  };

  const healthy = Object.values(results).every(v => v === 'ok');

  res.status(healthy ? 200 : 503).json({
    status: healthy ? 'ok' : 'degraded',
    checks: results,
  });
});

async function checkDatabase(): Promise<void> {
  await db.query('SELECT 1');
}

async function checkMessageQueue(): Promise<void> {
  await kafka.admin().describeCluster();
}

async function checkCache(): Promise<void> {
  await redis.ping();
}

Security Design

Inter-Service Authentication

// mTLS Configuration
import https from 'https';
import fs from 'fs';

const server = https.createServer({
  key: fs.readFileSync('/certs/server.key'),
  cert: fs.readFileSync('/certs/server.crt'),
  ca: fs.readFileSync('/certs/ca.crt'),
  requestCert: true,
  rejectUnauthorized: true,
}, app);

// JWT inter-service authentication
class ServiceAuthMiddleware {
  verify(req, res, next) {
    const token = req.headers['x-service-token'];

    try {
      const decoded = jwt.verify(token, process.env.SERVICE_SECRET);
      if (!decoded.service) {
        throw new Error('Invalid service token');
      }
      req.callingService = decoded.service;
      next();
    } catch {
      res.status(401).json({ error: 'Unauthorized service' });
    }
  }
}

Best Practices Summary

Microservices Best Practices:
┌─────────────────────────────────────────────────────┐
│                                                     │
│   Design Principles                                 │
│   ├── Decompose by business capability            │
│   ├── Keep services autonomous                     │
│   ├── Use API-first design                        │
│   └── Practice Domain-Driven Design               │
│                                                     │
│   Communication Patterns                            │
│   ├── Prefer asynchronous communication           │
│   ├── Use event-driven architecture               │
│   ├── Implement idempotency                       │
│   └── Design for fault tolerance                  │
│                                                     │
│   Data Management                                   │
│   ├── Database per service                        │
│   ├── Eventual consistency                        │
│   ├── Use Saga for distributed transactions       │
│   └── Consider CQRS and Event Sourcing           │
│                                                     │
│   Operations                                        │
│   ├── Comprehensive observability                 │
│   ├── Automated CI/CD                             │
│   ├── Containerized deployment                    │
│   └── Practice chaos engineering                  │
│                                                     │
└─────────────────────────────────────────────────────┘
PatternUse Case
API GatewayUnified entry, auth, rate limiting
Service DiscoveryDynamic service registration
Circuit BreakerFault isolation, graceful degradation
SagaDistributed transaction coordination
CQRSRead/write separation, performance optimization
Event SourcingAudit trail, state reconstruction

Microservices architecture brings flexibility but also introduces complexity. Start with a modular monolith and split into microservices only when truly needed.


Microservices are not the goal—they’re a means to an end. The key is solving business problems, not pursuing technical complexity.