Definition
- API Gateway – a single entry point handling routing, authentication, rate limiting, and request aggregation so clients never talk to services directly.
- Circuit Breaker – wraps remote calls, tracks failure rate over a sliding window, and flips CLOSED to OPEN to fail fast, then HALF_OPEN to test recovery.
- Database-per-Service – gives each service exclusive ownership of its data store, so services are decoupled at the data layer and evolve schemas independently.
- Reinforcement – these three interlock: DB-per-service forces network calls, network calls need circuit breakers, and the gateway is where you enforce auth and rate limits for all of them.
At National Bank of India, the API Gateway is the reception desk that checks your ID, limits how many people enter per minute, and points you to the right department. The Circuit Breaker is the "counter temporarily closed" sign: when the cash desk cannot serve, staff put up the sign so customers do not keep queuing and jamming the lobby. Database-per-Service is giving each department its own private ledger that no other department can open directly; if the loan department wants account data, it must ask the account department, not peek into its books.
Understanding the Concepts
What is the API Gateway pattern?
The API Gateway centralizes everything a client should not repeat. A route is a predicate plus filters. In Spring Cloud Gateway you route paths to services with filters for auth, rate limiting, and circuit breaking.
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 BankGatewayConfig {
@Bean
public RouteLocator bankRoutes(RouteLocatorBuilder builder) {
return builder.routes()
.route("payments", r -> r
.path("/api/v1/payments/**")
.filters(f -> f
.circuitBreaker(c -> c.setName("paymentCB"))
.requestRateLimiter(l -> l.setRateLimiter(redisRateLimiter())))
.uri("lb://payment-service"))
.route("accounts", r -> r
.path("/api/v1/accounts/**")
.uri("lb://account-service"))
.route("dashboard", r -> r
.path("/api/v1/dashboard")
.uri("lb://aggregation-service"))
.build();
}
}
Expected routing:
/api/v1/payments/PAY-001 -> payment-service (load balanced)
/api/v1/accounts/SAV2001 -> account-service (load balanced)
/api/v1/dashboard -> aggregation-service (fans out to multiple services)
The gateway can also aggregate: one client call to /api/v1/dashboard fans out to the account, payment, and card services and merges the responses, saving the mobile app three round trips.
How does the gateway hide internal topology?
The gateway hides the internal topology so you can split or rename a service without changing clients. The mobile app only knows about the gateway's public URL.
Before splitting loan from account:
Client -> Gateway -> /api/v1/accounts/** -> account-service
After splitting:
Client -> Gateway -> /api/v1/accounts/** -> account-service
Client -> Gateway -> /api/v1/loans/** -> loan-service (new)
Client code unchanged. Only gateway routes updated.
What is the Circuit Breaker pattern?
The Circuit Breaker protects callers from a failing dependency. It counts outcomes over a sliding window and transitions between states.
package com.bank.resilience;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.bulkhead.annotation.Bulkhead;
import io.github.resilience4j.retry.annotation.Retry;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.logging.Logger;
@Service
public class AccountServiceClient {
private static final Logger LOG = Logger.getLogger(AccountServiceClient.class.getName());
@CircuitBreaker(name = "accountService", fallbackMethod = "debitFallback")
@Retry(name = "accountService")
@Bulkhead(name = "accountService")
public void debit(String accountId, BigDecimal amount) {
// HTTP call to account-service
LOG.info("Debit of " + amount + " completed for " + accountId);
}
public void debitFallback(String accountId, BigDecimal amount, Throwable t) {
LOG.warning("Circuit OPEN. Returning queued-for-retry for " + accountId);
// Return a queued-for-retry response instead of failing the whole request
}
}
Circuit Breaker state machine: CLOSED ──(failure rate > 50%)──> OPEN ──(wait 60s)──> HALF_OPEN ^ │ └──────────(trial calls succeed)────────────────────────┘ HALF_OPEN ──(trial calls fail)──> OPEN
How do you configure a Circuit Breaker?
You always pair the circuit breaker with a timeout so a slow call counts as a failure, and often a bulkhead so the dependency cannot consume all threads.
# application.yml - Resilience4j configuration
resilience4j:
circuitbreaker:
instances:
accountService:
sliding-window-size: 10
failure-rate-threshold: 50
wait-duration-in-open-state: 60s
permitted-number-of-calls-in-half-open-state: 3
bulkhead:
instances:
accountService:
max-concurrent-calls: 10
retry:
instances:
accountService:
max-attempts: 3
wait-duration: 500ms
timelimiter:
instances:
accountService:
timeout-duration: 3s
Behavior under load:
- Max 10 concurrent calls (bulkhead)
- Each call times out at 3s (timeout counts as failure)
- Up to 3 retries with 500ms wait
- If 50% of last 10 calls fail -> circuit opens for 60s
- In HALF_OPEN, 3 trial calls test recovery
What is the Database-per-Service pattern?
The Payment Service owns the payments tables and the Account Service owns the accounts tables, with no shared schema and no cross-service joins.
package com.bank.payment.repository;
import com.bank.payment.model.Payment;
import org.springframework.data.jpa.repository.JpaRepository;
// Payment Service's private repository.
// Only Payment Service code accesses this.
public interface PaymentRepository extends JpaRepository<Payment, String> {
// No cross-service joins. No shared schema.
}
package com.bank.payment.model;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import java.math.BigDecimal;
import java.time.Instant;
@Entity
public class Payment {
@Id
private String paymentId;
private String fromAccountId; // ID reference, not a JPA relationship
private String toAccountId; // ID reference, not a JPA relationship
private BigDecimal amount;
private String status;
private Instant createdAt;
// The Payment Service stores fromAccountId as a plain string.
// It never JOINs with the Account Service's tables.
// If it needs the holder's name, it calls the Account Service
// or consumes account-events into a local read model.
}
Data access pattern:
Payment Service needs account holder name?
Option 1: Call account-service REST API (synchronous)
Option 2: Consume account-events from Kafka into local read model (async)
NEVER: SELECT from account-service's database directly
How do these three patterns reinforce each other?
These three patterns interlock. Database-per-Service forces services to talk over the network. Those network calls are exactly what the Circuit Breaker protects. And the API Gateway is the one place where you enforce authentication and rate limiting for all of them.
Client
│
▼
┌─────────────────────────────┐
│ API Gateway │ <-- Auth, rate limit
│ (Spring Cloud Gateway) │
└─────────────┬───────────────┘
│
┌────────┴────────┐
▼ ▼
┌──────────┐ ┌──────────────┐
│ Payment │ │ Account │
│ Service │──>│ Service │ <-- Circuit Breaker
└────┬─────┘ └──────┬───────┘
│ │
┌────┴─────┐ ┌──────┴───────┐
│Payment DB│ │ Account DB │ <-- DB-per-Service
└──────────┘ └──────────────┘
What is the tradeoff of Database-per-Service?
The cost is that you lose cross-service ACID transactions and must use the Saga pattern and eventual consistency.
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 AccountServiceClient accountClient;
public TransferSaga(AccountServiceClient accountClient) {
this.accountClient = accountClient;
}
public void execute(String fromId, String toId, BigDecimal amount) {
try {
accountClient.debit(fromId, amount); // Local tx in Account Service
accountClient.credit(toId, amount); // Local tx in Account Service
LOG.info("Transfer completed: " + fromId + " -> " + toId);
} catch (Exception e) {
accountClient.credit(fromId, amount); // Compensating action
LOG.warning("Transfer compensated: re-credited " + fromId);
}
}
}
Expected output (credit fails):
WARNING: Transfer compensated: re-credited SAV2001
Rajesh's money is returned. Eventual consistency is maintained.
What are common mistakes with these patterns?
The most common mistake is using Database-per-Service in name only while still sharing a database.
package com.bank.antipattern;
// WRONG: Payment Service queries Account Service's database directly.
public class WrongPaymentService {
// SELECT holder_name FROM accounts WHERE id = 'SAV2001'
// This creates hidden coupling. Schema changes in Account Service
// break Payment Service without warning.
}
// CORRECT: Payment Service calls Account Service API or reads from local copy.
public class CorrectPaymentService {
// accountClient.getAccountHolder("SAV2001");
// OR consume account-events into a local read-model table.
}
What should a senior engineer know?
The senior-level point is that these three patterns reinforce each other and are the minimum viable set for a production microservices architecture. Size is a distraction: the real test is whether a service can be deployed alone, and these three patterns are what make that possible.
Visual Overview
Three patterns and how they interlock:
┌──────────────────────────────────────────────────────┐
│ API GATEWAY │
│ Problem: centralize auth, routing, rate limiting │
│ Why needed: clients should not know internal topology│
└──────────────────────┬───────────────────────────────┘
│ routes to
┌────────────┴────────────┐
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ Payment Service │────►│ Account Service │
│ │ │ │
│ CIRCUIT BREAKER │ │ │
│ wraps this call │ │ │
└────────┬─────────┘ └────────┬─────────┘
│ │
┌────────┴─────────┐ ┌────────┴─────────┐
│ Payment DB │ │ Account DB │
│ (private) │ │ (private) │
│ DB-PER-SERVICE │ │ DB-PER-SERVICE │
└──────────────────┘ └──────────────────┘
DB-per-Service FORCES network calls
Network calls NEED Circuit Breakers
Gateway ENFORCES auth for all calls
Key Takeaway
When asked to explain microservices patterns in detail, I pick the three that nearly every real system needs and show how they reinforce each other. The API Gateway is the single entry point that handles routing, authentication, rate limiting, and request aggregation, so clients never talk to services directly. At National Bank of India, the mobile app makes one authenticated call to the gateway for Rajesh's dashboard, and the gateway fans out to the account and payment services and merges the responses.
The Circuit Breaker wraps a remote call and tracks the failure rate over a sliding window. When failures cross the threshold it flips from closed to open and every call fails instantly by invoking a fallback. I always pair it with a timeout so a slow call counts as a failure, and a bulkhead so one slow dependency does not consume all threads.
Database-per-Service gives each service exclusive ownership of its data store. The Payment Service owns the payments tables and the Account Service owns the accounts tables, with no shared schema and no cross-service joins. The tradeoff is that you lose cross-service ACID transactions and must use the Saga pattern with eventual consistency.
Remember one sentence: DB-per-Service forces network calls, network calls need Circuit Breakers, and the API Gateway enforces auth and rate limits for all of them in one place.
---