Field note / architecture

publishedupdated 2026-05-01#microservices#api-gateway#service-mesh#circuit-breaker#bulkhead

Microservices Patterns

Service decomposition, API gateway, service mesh, circuit breaker, bulkhead, sidecar, and the key trade-offs between microservices and monoliths.

Overview

Microservices decompose an application into small, independently deployable services each owning their data. The benefits are real — independent scaling, independent deployments, team autonomy — but so are the costs: distributed transactions, service discovery, network overhead, and operational complexity. The patterns on this page exist specifically to manage those costs.


Architecture

MICROSERVICES TOPOLOGY
════════════════════════════════════════════════════════

  External Clients
       │
  ┌────▼───────────────────────────────────────────────┐
  │              API Gateway                            │
  │  Auth, rate limit, routing, SSL termination        │
  │  Request fan-out / aggregation                     │
  └────┬───────────────────────────────────────────────┘
       │
  ┌────▼──────┐  ┌──────────────┐  ┌────────────────┐
  │  Order    │  │  Inventory   │  │  Payment       │
  │  Service  │  │  Service     │  │  Service       │
  │           │  │              │  │                │
  │  orders   │  │  inventory   │  │  payments      │
  │  DB       │  │  DB          │  │  DB            │
  └────┬──────┘  └──────────────┘  └────────────────┘
       │ Kafka (async events)
  ┌────▼──────────────────────────────────────────────┐
  │  Notification Service   │   Analytics Service     │
  └───────────────────────────────────────────────────┘

CIRCUIT BREAKER STATES
════════════════════════════════════════════════════════

  CLOSED (normal) → requests flow through, count failures
       │
       │ failure rate > threshold (e.g., 50% in 10 calls)
       ▼
  OPEN (tripped) → fail fast, no requests sent to service
       │           return fallback immediately
       │
       │ after wait period (e.g., 30s)
       ▼
  HALF-OPEN → allow 1 probe request
       │
       ├─▶ success → CLOSED (service recovered)
       └─▶ failure → OPEN again (still failing)

  Libraries: Resilience4j (Java), Hystrix (deprecated), Polly (.NET)

BULKHEAD PATTERN
════════════════════════════════════════════════════════
  Isolate thread pools per downstream service.
  Slow inventory service can't exhaust threads for payment service.

  Without bulkhead:              With bulkhead:
  threadPool: [all 100 threads]  threadPool-inventory: [20 threads]
  Inventory blocks 100 threads   threadPool-payment:   [20 threads]
  → payments also fail           threadPool-orders:    [20 threads]
                                 → inventory failure isolated

Technical Implementation

Resilience4j Circuit Breaker

@Configuration
public class ResilienceConfig {

  @Bean
  public CircuitBreakerRegistry circuitBreakerRegistry() {
    CircuitBreakerConfig config = CircuitBreakerConfig.custom()
        .failureRateThreshold(50)               // open at 50% failure rate
        .waitDurationInOpenState(Duration.ofSeconds(30))
        .slidingWindowType(COUNT_BASED)
        .slidingWindowSize(10)                  // evaluate last 10 calls
        .minimumNumberOfCalls(5)                // need at least 5 before evaluating
        .permittedNumberOfCallsInHalfOpenState(3)
        .recordExceptions(IOException.class, TimeoutException.class)
        .ignoreExceptions(BusinessValidationException.class)  // don't count these as failures
        .build();

    return CircuitBreakerRegistry.of(config);
  }
}

@Service
public class InventoryClient {

  private final CircuitBreaker cb;
  private final WebClient webClient;

  public InventoryClient(CircuitBreakerRegistry registry, WebClient webClient) {
    this.cb = registry.circuitBreaker("inventory");
    this.webClient = webClient;

    // Register event listeners for monitoring
    cb.getEventPublisher()
        .onStateTransition(e -> log.warn("Circuit breaker: {} → {}", e.getStateTransition()))
        .onCallNotPermitted(e -> metrics.increment("cb.rejected"));
  }

  public Mono<StockLevel> getStock(String productId) {
    return Mono.fromCallable(() ->
        cb.decorateSupplier(() -> fetchStock(productId)).get()
    )
    .onErrorResume(CallNotPermittedException.class, ex -> {
      log.warn("Circuit OPEN for inventory — using cached value");
      return stockCache.getOrDefault(productId, StockLevel.unknown());
    });
  }
}

API Gateway with Spring Cloud Gateway

@Configuration
public class GatewayConfig {

  @Bean
  public RouteLocator routes(RouteLocatorBuilder builder) {
    return builder.routes()
        // Order service route
        .route("order-service", r -> r
            .path("/api/orders/**")
            .filters(f -> f
                .rewritePath("/api/orders/(?<segment>.*)", "/orders/${segment}")
                .addRequestHeader("X-Gateway-Source", "api-gateway")
                .requestRateLimiter(c -> c
                    .setRateLimiter(redisRateLimiter())
                    .setKeyResolver(userKeyResolver())
                )
                .circuitBreaker(cb -> cb
                    .setName("order-cb")
                    .setFallbackUri("forward:/fallback/orders")
                )
            )
            .uri("lb://order-service")  // Kubernetes service discovery
        )

        // Inventory service with retry
        .route("inventory-service", r -> r
            .path("/api/inventory/**")
            .filters(f -> f
                .retry(retryConfig -> retryConfig
                    .setRetries(3)
                    .setMethods(HttpMethod.GET)
                    .setBackoff(Duration.ofMillis(100), Duration.ofSeconds(2), 2, true)
                )
            )
            .uri("lb://inventory-service")
        )
        .build();
  }

  @Bean
  public KeyResolver userKeyResolver() {
    return exchange -> Mono.justOrEmpty(
        exchange.getRequest().getHeaders().getFirst("X-User-Id")
    ).defaultIfEmpty("anonymous");
  }
}

Sidecar Pattern (Envoy/Istio Service Mesh)

# Kubernetes: Istio injects Envoy sidecar into every pod automatically
# All service-to-service traffic goes through Envoy sidecars

# VirtualService: traffic routing rules (canary deployment)
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: inventory-service
spec:
  http:
    - match:
        - headers:
            x-canary-user:
              exact: "true"
      route:
        - destination:
            host: inventory-service
            subset: v2
          weight: 100
    - route:
        - destination:
            host: inventory-service
            subset: v1
          weight: 90
        - destination:
            host: inventory-service
            subset: v2
          weight: 10

# DestinationRule: circuit breaker at mesh level (no code changes needed!)
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: inventory-service
spec:
  trafficPolicy:
    outlierDetection:
      consecutive5xxErrors: 5
      interval: 10s
      baseEjectionTime: 30s
      maxEjectionPercent: 50

Interview Preparation

Q: When would you NOT use microservices?

Microservices add complexity that only pays off at a certain scale of team and traffic. Don't use them for: a startup with one or two developers (a well-structured monolith is faster to build, easier to debug, cheaper to run), applications where domain boundaries aren't clear yet (premature decomposition creates wrong service splits that are expensive to undo), teams that don't have DevOps maturity (you need Kubernetes, CI/CD, distributed tracing, service discovery to operate microservices safely). The "modular monolith" is a valid intermediate step: maintain strict module boundaries inside one deployable unit, then extract services when you have evidence of bottleneck or team scaling needs.

Q: What is a service mesh and when do you need one?

A service mesh is an infrastructure layer that handles service-to-service communication — load balancing, mutual TLS, circuit breaking, retries, observability, canary routing — without any application code changes. It deploys an Envoy sidecar proxy alongside every service pod; all traffic between services goes through these sidecars. You need it when: you have 20+ services and adding resilience logic to each individually is impractical, when you need consistent mutual TLS (zero-trust networking) across all services, or when you need traffic management without code deploys (canary routing, A/B testing, fault injection). The cost: Istio adds ~200ms overhead to first connection, significant operational complexity, and resource overhead from sidecars.

Q: What is the bulkhead pattern and how does it prevent cascading failures?

Named after ship bulkheads (watertight compartments), the bulkhead pattern isolates resources for different consumers. In practice: separate thread pools (or connection pools) per downstream service. Without bulkhead, a slow database causing thread exhaustion will cascade — the same thread pool serves all downstream calls, so threads blocked on the slow DB prevent calls to fast services. With bulkhead, each downstream service gets its own thread pool of bounded size. A slow inventory service exhausts only its 20 threads; the payment service's 20 threads are unaffected. Combined with circuit breaker (which stops calling the slow service), cascading failure is contained.


Learning Resources