Back to Microservices Fundamentals
Q5Question 5 of 12

Common microservices design patterns

Key Concepts
  • API Gateway routes all requests through one entry point and handles auth, rate limiting, and aggregation.
  • Service Discovery lets services find each other by logical name instead of hardcoded addresses.
  • Circuit Breaker fails fast to prevent cascading failures when a dependency is unhealthy.
  • Saga manages distributed transactions with compensating actions instead of two-phase commit.
  • Event Sourcing stores state as an append-only log of events, enabling full audit history and replay.
  • CQRS separates the write model from the read model for different scaling and optimization needs.
  • Strangler Fig migrates a monolith incrementally by routing one capability at a time to a new service.

Definition

Gateway routes, Discovery finds, Circuit Breaker protects, Saga compensates, Event Sourcing logs, CQRS splits read/write, Strangler Fig migrates, DB-per-service isolates data, Sidecar helps, Bulkhead contains failure.
  • API Gateway – a single entry point for routing, authentication, rate limiting, and request aggregation.
  • Service Discovery – a registry that lets services find each other dynamically by logical name.
  • Circuit Breaker – fails fast when a dependency is unhealthy, preventing cascading failures.
  • Saga – manages distributed transactions with a sequence of local transactions and compensating actions.
  • Event Sourcing – stores state as an append-only log of immutable events instead of mutable rows.
  • CQRS – separates the write model from the read model so each can scale and optimize independently.
  • Strangler Fig – migrates a monolith incrementally by routing capabilities one at a time to new services.

At National Bank of India, API Gateway is the reception desk. Service Discovery is the campus directory. Circuit Breaker is the "counter closed" sign. Saga is the undo process when a two-step transfer half-fails. Event Sourcing is the passbook that records every transaction instead of only the final balance. CQRS is having a fast "balance display screen" separate from the slow "ledger writing desk". Strangler Fig is renovating the old bank one department at a time while it stays open. Bulkhead is keeping the loan queue and the transfer queue in separate rooms so a loan rush does not block transfers.

Understanding the Concepts

What is the API Gateway pattern?

The API Gateway pattern puts one entry point in front of all services. It handles routing, authentication, rate limiting, and request aggregation. Clients never talk to services directly.

JAVA
package com.bank.gateway;

import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class GatewayConfig {

    @Bean
    public RouteLocator routes(RouteLocatorBuilder builder) {
        return builder.routes()
            .route("payments", r -> r
                .path("/api/v1/payments/**")
                .filters(f -> f.circuitBreaker(c -> c.setName("paymentCB")))
                .uri("lb://payment-service"))
            .route("accounts", r -> r
                .path("/api/v1/accounts/**")
                .uri("lb://account-service"))
            .build();
    }
}
TEXT
Expected routing:
/api/v1/payments/PAY-001 -> payment-service (load balanced)
/api/v1/accounts/SAV2001 -> account-service (load balanced)

What is the Service Discovery pattern?

Services register under a logical name and callers resolve it. This solves the problem that instances come and go with scaling and deploys.

JAVA
package com.bank.payment;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import java.math.BigDecimal;

@FeignClient(name = "account-service")  // resolved via service registry
public interface AccountClient {

    @PostMapping("/api/v1/accounts/{id}/debit")
    void debit(@PathVariable String id, @RequestBody DebitRequest request);
}
TEXT
Expected behavior:
Feign resolves "account-service" via Eureka -> picks instance 192.168.1.10:8081
If that instance goes down, next call picks 192.168.1.11:8081 automatically.

What is the Circuit Breaker pattern?

The Circuit Breaker wraps a remote call and tracks its failure rate. When failures cross the threshold, it flips to OPEN and fails fast, then HALF_OPEN tests recovery.

JAVA
package com.bank.resilience;

import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.logging.Logger;

@Service
public class ProtectedAccountClient {

    private static final Logger LOG = Logger.getLogger(ProtectedAccountClient.class.getName());

    @CircuitBreaker(name = "accountService", fallbackMethod = "fallback")
    public void debit(String accountId, BigDecimal amount) {
        // Remote call to account-service
        LOG.info("Debit completed for " + accountId);
    }

    public void fallback(String accountId, BigDecimal amount, Throwable t) {
        LOG.warning("Circuit OPEN for accountService. Fallback for " + accountId);
    }
}
TEXT
State transitions:
CLOSED -> failure rate > threshold -> OPEN (fail fast)
OPEN -> wait duration expires -> HALF_OPEN (permit trial calls)
HALF_OPEN -> trial calls succeed -> CLOSED

What is the Saga pattern?

The Saga pattern replaces two-phase commit. A payment saga debits then credits, and on failure runs a compensating action.

JAVA
package com.bank.saga;

import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.logging.Logger;

@Service
public class TransferSaga {

    private static final Logger LOG = Logger.getLogger(TransferSaga.class.getName());
    private final AccountClient accountClient;

    public TransferSaga(AccountClient accountClient) {
        this.accountClient = accountClient;
    }

    public void execute(String fromId, String toId, BigDecimal amount) {
        try {
            accountClient.debit(fromId, amount);  // Step 1
            accountClient.credit(toId, amount);   // Step 2
            LOG.info("Saga completed: " + fromId + " -> " + toId);
        } catch (Exception e) {
            accountClient.credit(fromId, amount);  // Compensation
            LOG.warning("Saga compensated: re-credited " + fromId);
        }
    }
}
TEXT
Expected output (failure case):
WARNING: Saga compensated: re-credited SAV2001

What is Event Sourcing?

Event Sourcing stores every change as an immutable event. Instead of updating a balance column, you append AccountDebited and AccountCredited events and rebuild the balance by replaying them.

JAVA
package com.bank.eventsourcing;

import java.math.BigDecimal;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;

public class EventSourcedAccount {

    private final String accountId;
    private BigDecimal balance = BigDecimal.ZERO;
    private final List<DomainEvent> uncommittedEvents = new ArrayList<>();

    public EventSourcedAccount(String accountId) {
        this.accountId = accountId;
    }

    public void debit(BigDecimal amount) {
        if (balance.compareTo(amount) < 0) {
            throw new IllegalStateException("Insufficient funds in " + accountId);
        }
        apply(new AccountDebited(accountId, amount, Instant.now()));
    }

    private void apply(DomainEvent event) {
        if (event instanceof AccountDebited d) {
            balance = balance.subtract(d.amount());
        } else if (event instanceof AccountCredited c) {
            balance = balance.add(c.amount());
        }
        uncommittedEvents.add(event);
    }

    // Replay from event store to reconstruct state
    public static EventSourcedAccount replayFrom(String accountId, List<DomainEvent> history) {
        EventSourcedAccount account = new EventSourcedAccount(accountId);
        history.forEach(account::apply);
        account.uncommittedEvents.clear();
        return account;
    }
}
TEXT
Expected event store:
1. AccountCredited(SAV2001, 100000.00, 2024-01-01T10:00:00Z)
2. AccountDebited(SAV2001, 50000.00, 2024-01-02T14:30:00Z)
3. AccountCredited(SAV2001, 25000.00, 2024-01-03T09:15:00Z)
Replay -> balance = 100000 - 50000 + 25000 = 75000.00

What is CQRS?

CQRS separates writes from reads. The command side appends events while a read model is projected for fast queries.

JAVA
package com.bank.cqrs;

import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
import java.util.logging.Logger;

// Read-side projection: updates a materialized balance view
@Component
public class BalanceProjection {

    private static final Logger LOG = Logger.getLogger(BalanceProjection.class.getName());
    private final BalanceViewRepository viewRepo;

    public BalanceProjection(BalanceViewRepository viewRepo) {
        this.viewRepo = viewRepo;
    }

    @KafkaListener(topics = "account-events", groupId = "balance-projection")
    public void onEvent(AccountEvent event) {
        viewRepo.updateBalance(event.getAccountId(), event.getNewBalance());
        LOG.info("Balance view updated for " + event.getAccountId());
    }
}
TEXT
Command side: append events to event store
Read side: project into balance_view table for fast SELECT queries
Write scaling != Read scaling (optimize independently)

What is the Strangler Fig pattern?

The Strangler Fig migrates a monolith gradually. The gateway routes some paths to a new service while the rest still hit the monolith, and you move one capability at a time until the monolith is gone.

TEXT
Phase 1: All traffic goes to monolith
  /api/v1/payments/** -> monolith
  /api/v1/accounts/** -> monolith

Phase 2: Extract Payment Service
  /api/v1/payments/** -> payment-service (new)
  /api/v1/accounts/** -> monolith

Phase 3: Extract Account Service
  /api/v1/payments/** -> payment-service
  /api/v1/accounts/** -> account-service (new)

Phase 4: Monolith deleted

What are the Bulkhead and Sidecar patterns?

The Bulkhead pattern isolates resource pools so one failure cannot sink everything. The Sidecar pattern deploys a helper container beside each service for logging, proxying, or TLS.

JAVA
package com.bank.resilience;

import io.github.resilience4j.bulkhead.annotation.Bulkhead;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.logging.Logger;

@Service
public class BulkheadAccountClient {

    private static final Logger LOG = Logger.getLogger(BulkheadAccountClient.class.getName());

    @Bulkhead(name = "accountService", fallbackMethod = "bulkheadFallback")
    public void debit(String accountId, BigDecimal amount) {
        LOG.info("Debit call within bulkhead for " + accountId);
    }

    public void bulkheadFallback(String accountId, BigDecimal amount, Throwable t) {
        LOG.warning("Bulkhead full for accountService. Rejecting call for " + accountId);
    }
}
TEXT
Bulkhead: max 10 concurrent calls to account-service
  -> 11th call rejected immediately, does not consume thread pool
Sidecar (Istio/Envoy): runs as co-located container
  -> handles mTLS, observability, retries outside application code

How do you choose which pattern to use?

Use the smallest pattern that solves your problem. Event Sourcing and CQRS add real complexity and are justified only when you need full audit history or very different read and write scaling.

| Pattern | Problem Solved | When to Use |
|---|---|---|
| API Gateway | Centralize routing, auth, rate limiting | Always in production |
| Service Discovery | Dynamic instance addresses | Always with multiple instances |
| Circuit Breaker | Cascading failures | Every remote call |
| Saga | Distributed transactions | Cross-service business operations |
| Event Sourcing | Audit trail, replay, temporal queries | Regulatory audit, financial ledgers |
| CQRS | Different read/write scaling | Heavy read workloads, complex queries |
| Strangler Fig | Monolith migration | Incremental modernization |
| Database-per-Service | Data coupling | Always; foundation of microservices |
| Bulkhead | Resource isolation | When one dependency can starve others |
| Sidecar | Cross-cutting infra concerns | Service mesh (Istio, Linkerd) |

Visual Overview

DIAGRAM
  Patterns by problem category:

  ENTRY & DISCOVERY          RESILIENCE             DATA & TRANSACTIONS
  ┌──────────────┐      ┌──────────────┐      ┌──────────────────────┐
  │ API Gateway  │      │Circuit Breaker│      │ Saga + Compensation  │
  │ (routing,    │      │ (fail fast)  │      │ (eventual consistency)│
  │  auth, rate) │      ├──────────────┤      ├──────────────────────┤
  ├──────────────┤      │  Bulkhead    │      │ Event Sourcing       │
  │  Service     │      │ (isolation)  │      │ (append-only events) │
  │  Discovery   │      └──────────────┘      ├──────────────────────┤
  │ (Eureka)     │                            │ CQRS                 │
  └──────────────┘      MIGRATION             │ (read/write split)   │
                        ┌──────────────┐      ├──────────────────────┤
  DEPLOYMENT            │ Strangler Fig│      │ DB-per-Service       │
  ┌──────────────┐      │ (incremental)│      │ (data isolation)     │
  │   Sidecar    │      └──────────────┘      └──────────────────────┘
  │ (Envoy/mesh) │
  └──────────────┘

Key Takeaway

Microservices design patterns are reusable solutions to the recurring problems of distributed systems, and I group them by the problem they solve. For entry and discovery, the API Gateway gives one front door for routing and authentication, and Service Discovery lets services find each other by logical name. For resilience, the Circuit Breaker fails fast when a dependency is down, and the Bulkhead isolates resource pools so a surge in one area cannot starve the rest.

For data and transactions, the Saga pattern replaces two-phase commit by running steps with compensating actions. At National Bank of India, a transfer saga debits Rajesh then credits Priya, and if the credit fails it re-credits Rajesh. Database-per-Service keeps each service's store private. Event Sourcing stores every change as an immutable event so auditors can see the full history, and CQRS separates the write model from the read model for independent scaling.

For migration, the Strangler Fig pattern lets you move a monolith to microservices one capability at a time. For deployment, the Sidecar pattern co-locates a helper container for TLS, logging, and tracing, which is how a service mesh like Istio works with Envoy.

Remember one sentence: choose the smallest pattern that solves your problem, because reaching for Event Sourcing and CQRS on day one produces a system harder to operate than the monolith it replaced.

---

Share
Use ← → arrow keys to navigate