Back to API Gateway
Q13Question 13 of 16

Core Microservices: API Gateway, Saga, idempotency, circuit breaker, distributed tracing

Key Concepts
  • Microservices are independently deployable services, each owning its own database and communicating via APIs or events.
  • API Gateway is the single entry point handling routing, authentication, rate limiting, and circuit breaking.
  • Saga pattern manages distributed transactions via compensating actions when @Transactional cannot span services.
  • Idempotency keys prevent duplicate payments on retries by storing and returning the original response.
  • Circuit Breaker (Resilience4j) prevents cascading failures: CLOSED (normal), OPEN (fail-fast), HALF_OPEN (test recovery).
  • Distributed tracing (Micrometer + Zipkin) correlates requests across services via a single traceId.
  • Database-per-service ensures independence; services communicate through APIs, not shared tables.

Definition

Microservices are independently deployable services, each owning its database, connected by an API Gateway, protected by circuit breakers, coordinated by Sagas, and observed through distributed tracing.
  • Microservices – independently deployable services, each owning its data and communicating via REST, gRPC, or Kafka.
  • API Gateway – single entry point handling routing, authentication, rate limiting, and circuit breaking (Spring Cloud Gateway, Kong).
  • Saga pattern – manages distributed transactions through compensating actions because @Transactional cannot span services.
  • Idempotency keys – prevent duplicate payments on retries by storing the response for each unique key.
  • Circuit Breaker – Resilience4j pattern that moves from CLOSED to OPEN after a failure threshold, failing fast instead of cascading.
  • Distributed tracing – Micrometer propagates a traceId across all services so one request's journey is visible in Zipkin.

Instead of one giant bank building handling everything, National Bank of India has separate departments: Account Service, Payment Service, Notification Service. The API Gateway is the reception desk routing every customer. The Saga pattern handles rollbacks: if Rajesh's payment fails after his account was debited, the compensation re-credits his account SAV2001. The circuit breaker is the manager who stops sending customers to a broken counter. The traceId is the reference number stamped on every form so the whole journey is visible.

Understanding the Concepts

What are microservices?

Microservices break a monolith into small services that each own their data and deploy independently. The practical rule is one database per service, so the Payment Service never reaches into the Account Service's tables; it calls the service instead.

JAVA
package com.bank.payment;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class PaymentServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(PaymentServiceApplication.class, args);
    }
}
// Owns: payment_db (PostgreSQL)
// Communicates: REST to Account Service, Kafka to Notification Service
// Deploys: independently, own Docker container, own scaling

How does the API Gateway work?

The API Gateway is the single front door. It routes requests, validates JWTs, enforces rate limits, and applies circuit breakers. Clients talk to one address; the gateway handles the rest.

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(cb -> cb
                                        .setName("paymentCB")
                                        .setFallbackUri("forward:/fallback/payments"))
                                .requestRateLimiter(rl -> rl
                                        .setRateLimiter(redisRateLimiter())))
                        .uri("lb://payment-service"))
                .build();
    }

    private Object redisRateLimiter() {
        // Redis-backed token bucket: 100 requests/second per client
        return null; // Configuration object
    }
}
DIAGRAM
// Client calls: POST /api/v1/payments/transfer
// Gateway: validates JWT → checks rate limit → routes to payment-service
// If payment-service is down → circuit breaker → fallback response

How does the Saga pattern manage distributed transactions?

A Saga models the operation as a sequence of local transactions. Each step commits in its own service, and on failure, compensating transactions undo the earlier committed steps.

JAVA
package com.bank.saga;

import org.springframework.stereotype.Service;
import java.math.BigDecimal;

@Service
public class PaymentSagaOrchestrator {

    private final AccountClient accountClient;
    private final PaymentRepository paymentRepo;

    public PaymentSagaOrchestrator(AccountClient accountClient,
                                     PaymentRepository paymentRepo) {
        this.accountClient = accountClient;
        this.paymentRepo = paymentRepo;
    }

    public String executeTransfer(String from, String to, BigDecimal amount) {
        // T1: Debit sender
        accountClient.debit(from, amount);

        try {
            // T2: Credit receiver
            accountClient.credit(to, amount);
            return paymentRepo.recordSuccess(from, to, amount);
        } catch (Exception e) {
            // C1: Compensate - re-credit sender
            accountClient.credit(from, amount);
            throw new TransferFailedException(
                    "Credit failed, debit compensated: " + e.getMessage());
        }
    }
}
DIAGRAM
// Happy: debit SAV2001 → credit SAV3002 → SUCCESS
// Failure: debit SAV2001 → credit fails → re-credit SAV2001 → COMPENSATED

How do idempotency keys prevent duplicate payments?

The client sends a UUID with each payment request. Before processing, the service checks if that key was already handled and returns the stored response.

JAVA
package com.bank.service;

import org.springframework.stereotype.Service;
import java.math.BigDecimal;

@Service
public class IdempotentPaymentService {

    private final IdempotencyRepository idempotencyRepo;
    private final PaymentProcessor processor;

    public IdempotentPaymentService(IdempotencyRepository idempotencyRepo,
                                      PaymentProcessor processor) {
        this.idempotencyRepo = idempotencyRepo;
        this.processor = processor;
    }

    public PaymentResult processPayment(String idempotencyKey,
                                          String accountId, BigDecimal amount) {
        // Check: was this key already processed?
        var existing = idempotencyRepo.findByKey(idempotencyKey);
        if (existing != null) {
            return existing.getResult(); // Return stored response
        }

        // Process the payment
        PaymentResult result = processor.charge(accountId, amount);

        // Store the result for this key
        idempotencyRepo.save(idempotencyKey, result);
        return result;
    }
}
DIAGRAM
// First call:  key=UUID-001 → processes payment → stores result → returns OK
// Retry call:  key=UUID-001 → finds stored result → returns same OK
// Customer is never charged twice, even if the client retries

How does the circuit breaker prevent cascading failures?

Resilience4j wraps downstream calls. After a failure threshold, it opens the circuit and fails fast, protecting the calling service from hanging.

JAVA
package com.bank.service;

import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.retry.annotation.Retry;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;

@Service
public class ResilientAccountClient {

    private final AccountClient accountClient;

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

    @CircuitBreaker(name = "accountService", fallbackMethod = "fallback")
    @Retry(name = "accountService")
    public BigDecimal getBalance(String accountId) {
        return accountClient.getBalance(accountId);
    }

    private BigDecimal fallback(String accountId, Throwable t) {
        throw new ServiceUnavailableException(
                "Account Service down for " + accountId);
    }
}
DIAGRAM
// CLOSED: calls go through normally
// 5 failures in 10 calls → OPEN: all calls fail fast (no network)
// After 30 seconds → HALF_OPEN: 3 test calls allowed
// Test calls succeed → CLOSED: normal operation resumes

How does distributed tracing work?

Micrometer Tracing propagates a single traceId across all services. Every log line carries it, and Zipkin draws the end-to-end waterfall.

JAVA
// With Micrometer Tracing + Zipkin:
// Gateway log:  traceId=abc123 - Routing to payment-service
// Payment log:  traceId=abc123 - Processing transfer TXN-456
// Account log:  traceId=abc123 - Debiting SAV2001 by 50000
// Notification:  traceId=abc123 - Sending SMS to Priya

// Search logs for traceId=abc123 → see the entire request journey
// Zipkin UI: visual waterfall showing latency per service

What is the database-per-service principle?

Each service owns its database. No other service reads or writes to it directly. This ensures independence and allows each service to choose the best database technology.

DIAGRAM
// Account Service  → account_db (PostgreSQL) → accounts, balances
// Payment Service  → payment_db (PostgreSQL) → transfers, idempotency
// Notification Svc → notification_db (MongoDB) → templates, delivery log
// Analytics Svc    → analytics_db (ClickHouse) → events, metrics

// WRONG: Payment Service reads from account_db directly
// CORRECT: Payment Service calls Account Service API for balance

What should a senior engineer know?

A senior engineer ties all these patterns together. The API Gateway is the front door. Inside, services communicate synchronously (REST/gRPC) for queries and asynchronously (Kafka) for events. The Saga manages consistency across services. Idempotency keys make retries safe. Circuit breakers prevent cascading failures. Distributed tracing makes the whole system observable.

JAVA
// Production transfer flow at National Bank of India:
// 1. Client sends POST /api/v1/transfers with idempotency key UUID-001
// 2. Gateway: validates JWT, rate limits via Redis, routes to payment-service
// 3. Payment Service: checks idempotency key → not processed yet
// 4. Saga: debit SAV2001 (sync) → credit SAV3002 (sync)
//    If credit fails → compensate: re-credit SAV2001
// 5. Publish TransferCompleted to Kafka (async)
// 6. Notification Service: consumes event → sends SMS to Rajesh
// 7. Distributed tracing: traceId=abc123 visible in Zipkin
// 8. Store idempotency key + result → retries return same response

Visual Overview

DIAGRAM
  Core Microservices Architecture
  ────────────────────────────────
  Mobile App / Web App
         │
         ▼
  ┌─────────────────────────────────────┐
  │           API Gateway                │
  │  JWT Auth │ Rate Limit │ Circuit Brk │
  │  Routing  │ Tracing    │ Load Balance│
  └──────┬──────────┬──────────┬────────┘
         │          │          │
         ▼          ▼          ▼
  ┌──────────┐ ┌─────────┐ ┌──────────────┐
  │ Account  │ │ Payment │ │ Notification │
  │ Service  │ │ Service │ │   Service    │
  │[acct_db] │ │[pay_db] │ │ [notif_db]  │
  └──────────┘ └────┬────┘ └──────▲───────┘
                    │              │
                    │   Kafka      │
                    └──────────────┘

  Saga Compensation Flow
  ──────────────────────
  Orchestrator
      │── debit SAV2001 ──▶ Account Svc ✓
      │── credit SAV3002 ──▶ Account Svc ✗ (FAILS)
      │── compensate: re-credit SAV2001 ──▶ Account Svc ✓
      │ State: COMPENSATED

  Circuit Breaker States
  ──────────────────────
  CLOSED ──(failures)──▶ OPEN ──(wait)──▶ HALF_OPEN ──(success)──▶ CLOSED
                                              │
                                              └──(failure)──▶ OPEN

Key Takeaway

Microservices are independently deployable services, each owning its database. The API Gateway is the single entry point handling routing, authentication, rate limiting, and circuit breaking. The Saga pattern manages distributed transactions because @Transactional cannot span services: if step 2 fails, compensating actions undo step 1. Idempotency keys prevent duplicate payments on retries by storing the response for each key and returning the stored response on subsequent calls.

At National Bank of India, Rajesh's transfer request flows through the API Gateway, which validates his JWT, applies rate limiting via Redis, and routes to the Payment Service. The Payment Service checks the idempotency key, executes the Saga (debit SAV2001, credit SAV3002), and on failure compensates by re-crediting the sender. After success, a TransferCompleted event is published to Kafka for the Notification Service to consume asynchronously. Distributed tracing with Micrometer propagates the traceId across all services so the full journey is visible in Zipkin.

The patterns work together as a system: the gateway secures and routes, the saga keeps data consistent, idempotency makes retries safe, the circuit breaker prevents cascading failures, and distributed tracing makes everything observable. Each pattern solves one problem, and together they make a distributed system behave reliably.

Remember one sentence: microservices are independent services connected by an API Gateway, kept consistent by Sagas, made retry-safe by idempotency keys, protected by circuit breakers, and observed through distributed tracing.

---

Share
Use ← → arrow keys to navigate