Files
2026-04-12 01:06:31 +07:00

12 KiB

Backend Architecture Patterns

Microservices, event-driven architecture, and scalability patterns (2025).

Monolith vs Microservices

Monolithic Architecture

┌─────────────────────────────────┐
│      Single Application         │
│                                 │
│  ┌─────────┐  ┌──────────┐    │
│  │  Users  │  │ Products │    │
│  └─────────┘  └──────────┘    │
│  ┌─────────┐  ┌──────────┐    │
│  │ Orders  │  │ Payments │    │
│  └─────────┘  └──────────┘    │
│                                 │
│     Single Database             │
└─────────────────────────────────┘

Pros:

  • Simple to develop and deploy
  • Easy local testing
  • Single codebase
  • Strong consistency (ACID transactions)

Cons:

  • Tight coupling
  • Scaling limitations
  • Deployment risk (all-or-nothing)
  • Tech stack lock-in

When to Use: Startups, MVPs, small teams, unclear domain boundaries

Microservices Architecture

┌──────────┐   ┌──────────┐   ┌──────────┐   ┌──────────┐
│  User    │   │ Product  │   │  Order   │   │ Payment  │
│ Service  │   │ Service  │   │ Service  │   │ Service  │
└────┬─────┘   └────┬─────┘   └────┬─────┘   └────┬─────┘
     │              │              │              │
  ┌──▼──┐        ┌──▼──┐        ┌──▼──┐        ┌──▼──┐
  │  DB │        │  DB │        │  DB │        │  DB │
  └─────┘        └─────┘        └─────┘        └─────┘

Pros:

  • Independent deployment
  • Technology flexibility
  • Fault isolation
  • Easier scaling (scale services independently)

Cons:

  • Complex deployment
  • Distributed system challenges (network latency, partial failures)
  • Data consistency (eventual consistency)
  • Operational overhead

When to Use: Large teams, clear domain boundaries, need independent scaling, tech diversity

Microservices Patterns

Database per Service Pattern

Concept: Each service owns its database

User Service → User DB (PostgreSQL)
Product Service → Product DB (MongoDB)
Order Service → Order DB (PostgreSQL)

Benefits:

  • Service independence
  • Technology choice per service
  • Fault isolation

Challenges:

  • No joins across services
  • Distributed transactions
  • Data duplication

API Gateway Pattern

Client
  │
  ▼
┌─────────────────┐
│  API Gateway    │  - Authentication
│  (Kong/NGINX)   │  - Rate limiting
└────────┬────────┘  - Request routing
         │
    ┌────┴────┬────────┬────────┐
    ▼         ▼        ▼        ▼
  User    Product   Order   Payment
 Service  Service  Service  Service

Responsibilities:

  • Request routing
  • Authentication/authorization
  • Rate limiting
  • Request/response transformation
  • Caching

Implementation (Kong):

services:
  - name: user-service
    url: http://user-service:3000
    routes:
      - name: user-route
        paths:
          - /api/users

  - name: product-service
    url: http://product-service:3001
    routes:
      - name: product-route
        paths:
          - /api/products

plugins:
  - name: rate-limiting
    config:
      minute: 100
  - name: jwt

Service Discovery

Concept: Services find each other dynamically

// Consul service discovery
import Consul from 'consul';

const consul = new Consul();

// Register service
await consul.agent.service.register({
  name: 'user-service',
  address: '192.168.1.10',
  port: 3000,
  check: {
    http: 'http://192.168.1.10:3000/health',
    interval: '10s',
  },
});

// Discover service
const services = await consul.catalog.service.nodes('product-service');
const productServiceUrl = `http://${services[0].ServiceAddress}:${services[0].ServicePort}`;

Circuit Breaker Pattern

Concept: Stop calling failing service, prevent cascade failures

import CircuitBreaker from 'opossum';

const breaker = new CircuitBreaker(callExternalService, {
  timeout: 3000, // 3s timeout
  errorThresholdPercentage: 50, // Open circuit after 50% failures
  resetTimeout: 30000, // Try again after 30s
});

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

breaker.fallback(() => ({
  data: 'fallback-response',
  source: 'cache',
}));

const result = await breaker.fire(requestParams);

States:

  • Closed: Normal operation, requests go through
  • Open: Too many failures, requests fail immediately
  • Half-Open: Testing if service recovered

Saga Pattern (Distributed Transactions)

Choreography-Based Saga:

Order Service: Create Order → Publish "OrderCreated"
                                    ↓
Payment Service: Reserve Payment → Publish "PaymentReserved"
                                    ↓
Inventory Service: Reserve Stock → Publish "StockReserved"
                                    ↓
Shipping Service: Create Shipment → Publish "ShipmentCreated"

If any step fails → Compensating transactions (rollback)

Orchestration-Based Saga:

Saga Orchestrator
    ↓ Create Order
Order Service
    ↓ Reserve Payment
Payment Service
    ↓ Reserve Stock
Inventory Service
    ↓ Create Shipment
Shipping Service

Event-Driven Architecture

Impact: 85% organizations recognize business value

Event Sourcing

Concept: Store events, not current state

// Traditional: Store current state
{
  userId: '123',
  balance: 500
}

// Event Sourcing: Store events
[
  { type: 'AccountCreated', userId: '123', timestamp: '...' },
  { type: 'MoneyDeposited', amount: 1000, timestamp: '...' },
  { type: 'MoneyWithdrawn', amount: 500, timestamp: '...' },
]

// Reconstruct state by replaying events
const balance = events
  .filter(e => e.userId === '123')
  .reduce((acc, event) => {
    if (event.type === 'MoneyDeposited') return acc + event.amount;
    if (event.type === 'MoneyWithdrawn') return acc - event.amount;
    return acc;
  }, 0);

Benefits:

  • Complete audit trail
  • Temporal queries (state at any point in time)
  • Event replay for debugging
  • Flexible projections

Message Broker Patterns

Kafka (Event Streaming):

import { Kafka } from 'kafkajs';

const kafka = new Kafka({
  clientId: 'order-service',
  brokers: ['kafka:9092'],
});

// Producer
const producer = kafka.producer();
await producer.send({
  topic: 'order-events',
  messages: [
    {
      key: order.id,
      value: JSON.stringify({
        type: 'OrderCreated',
        orderId: order.id,
        userId: order.userId,
        total: order.total,
      }),
    },
  ],
});

// Consumer
const consumer = kafka.consumer({ groupId: 'inventory-service' });
await consumer.subscribe({ topic: 'order-events' });
await consumer.run({
  eachMessage: async ({ topic, partition, message }) => {
    const event = JSON.parse(message.value.toString());
    if (event.type === 'OrderCreated') {
      await reserveInventory(event.orderId);
    }
  },
});

RabbitMQ (Task Queues):

import amqp from 'amqplib';

const connection = await amqp.connect('amqp://localhost');
const channel = await connection.createChannel();

// Producer
await channel.assertQueue('email-queue', { durable: true });
channel.sendToQueue('email-queue', Buffer.from(JSON.stringify({
  to: user.email,
  subject: 'Welcome!',
  body: 'Thank you for signing up',
})));

// Consumer
await channel.consume('email-queue', async (msg) => {
  const emailData = JSON.parse(msg.content.toString());
  await sendEmail(emailData);
  channel.ack(msg);
});

CQRS (Command Query Responsibility Segregation)

Concept: Separate read and write models

Write Side (Commands):           Read Side (Queries):
CreateOrder                      GetOrderById
UpdateOrder                      GetUserOrders
  ↓                                ↑
┌─────────┐                    ┌─────────┐
│ Write   │ → Events →         │  Read   │
│  DB     │    (sync)          │  DB     │
│(Postgres)                    │(MongoDB)│
└─────────┘                    └─────────┘

Benefits:

  • Optimized read models
  • Scalable (scale reads independently)
  • Flexible (different DB for reads/writes)

Implementation:

// Command (Write)
class CreateOrderCommand {
  constructor(public userId: string, public items: OrderItem[]) {}
}

class CreateOrderHandler {
  async execute(command: CreateOrderCommand) {
    const order = await Order.create(command);
    await eventBus.publish(new OrderCreatedEvent(order));
    return order.id;
  }
}

// Query (Read)
class GetOrderQuery {
  constructor(public orderId: string) {}
}

class GetOrderHandler {
  async execute(query: GetOrderQuery) {
    // Read from optimized read model
    return await OrderReadModel.findById(query.orderId);
  }
}

Scalability Patterns

Horizontal Scaling (Scale Out)

Load Balancer
    ↓
┌───┴───┬───────┬───────┐
│ App 1 │ App 2 │ App 3 │ ... App N
└───┬───┴───┬───┴───┬───┘
    └───────┴───────┘
         ↓
    Shared Database
    (with read replicas)

Database Sharding

Range-Based Sharding:

Users 1-1M     → Shard 1
Users 1M-2M    → Shard 2
Users 2M-3M    → Shard 3

Hash-Based Sharding:

function getShardId(userId: string): number {
  const hash = crypto.createHash('md5').update(userId).digest('hex');
  return parseInt(hash.substring(0, 8), 16) % SHARD_COUNT;
}

const shardId = getShardId(userId);
const db = shards[shardId];
const user = await db.users.findById(userId);

Caching Layers

Client
  → CDN (static assets)
  → API Gateway Cache (public endpoints)
  → Application Cache (Redis - user sessions, hot data)
  → Database Query Cache
  → Database

Architecture Decision Matrix

Pattern When to Use Complexity Benefits
Monolith Small team, MVP, unclear boundaries Low Simple, fast development
Microservices Large team, clear domains, need scaling High Independent deployment, fault isolation
Event-Driven Async workflows, audit trail needed Moderate Decoupling, scalability
CQRS Different read/write patterns High Optimized queries, scalability
Serverless Spiky traffic, event-driven Low Auto-scaling, pay-per-use

Anti-Patterns to Avoid

  1. Distributed Monolith - Microservices that all depend on each other
  2. Chatty Services - Too many inter-service calls (network overhead)
  3. Shared Database - Microservices sharing same DB (tight coupling)
  4. Over-Engineering - Using microservices for small apps
  5. No Circuit Breakers - Cascade failures in distributed systems

Architecture Checklist

  • Clear service boundaries (domain-driven design)
  • Database per service (no shared databases)
  • API Gateway for client requests
  • Service discovery configured
  • Circuit breakers for resilience
  • Event-driven communication (Kafka/RabbitMQ)
  • CQRS for read-heavy systems
  • Distributed tracing (Jaeger/OpenTelemetry)
  • Health checks for all services
  • Horizontal scaling capability

Resources