API Gateway & Service Communication
Module 19 handled the fire-and-forget case. This module is for when a client genuinely needs a synchronous answer, and you have several services instead of one.
Learning objectives
- Beginner: Explain what problem an API Gateway solves that calling each microservice directly from the client doesn't.
- Intermediate: Implement a synchronous service-to-service REST call and handle a timeout from the downstream service.
- Advanced: Design a gateway routing/resilience strategy (rate limiting, circuit breaking) so one failing downstream service doesn't cascade into a total outage.
◆ The problem
With separate Order, Inventory, and Notification services (Module 18) each running on their own port, a client (a mobile app, a frontend) would need to know every individual service's address, handle auth separately against each, and adapt as services are added, removed, or moved — a maintenance and security surface that grows with every new service.
An API Gateway is a single entry point that sits in front of every backend service, routing each incoming request to the correct internal service based on its path — clients only ever need to know the gateway's address, never the individual services behind it.
| Gateway responsibility | Why it belongs here, not in each service |
|---|---|
| Routing | One place mapping /orders/** → Order service, /inventory/** → Inventory service. |
| Authentication | Verify the JWT (Module 13) once at the gateway, rather than duplicating token-validation logic in every downstream service. |
| Rate limiting | One consistent policy across the whole system, instead of per-service reimplementation. |
| Cross-cutting logging/tracing | A single place to log every request into the system, useful for observability regardless of which service eventually handles it. |
spring: cloud: gateway: routes: - id: order-service uri: http://localhost:8081 predicates: - Path=/orders/** - id: inventory-service uri: http://localhost:8082 predicates: - Path=/inventory/**
💻 Code example
spring: cloud: gateway: routes: - id: order-service uri: http://localhost:8081 predicates: - Path=/orders/** - id: inventory-service uri: http://localhost:8082 predicates: - Path=/inventory/**
Downstream of the gateway, services sometimes still need to call each other directly and synchronously (e.g. Order service checking current stock with Inventory service before confirming an order) — RestClient (Spring's modern HTTP client, superseding RestTemplate) is the standard tool for this.
@Service public class OrderService { private final RestClient restClient; public OrderService(RestClient.Builder builder) { this.restClient = builder.baseUrl("http://inventory-service:8082").build(); } private boolean hasStock(Long productId, int quantity) { StockResponse stock = restClient.get() .uri("/inventory/{id}", productId) .retrieve() .body(StockResponse.class); return stock.available() >= quantity; } }
💻 Code example
@Service public class OrderService { private final RestClient restClient; public OrderService(RestClient.Builder builder) { this.restClient = builder.baseUrl("http://inventory-service:8082").build(); } private boolean hasStock(Long productId, int quantity) { StockResponse stock = restClient.get() .uri("/inventory/{id}", productId) .retrieve() .body(StockResponse.class); return stock.available() >= quantity; } }
◆ The problem
A synchronous call to Inventory service means the Order service's own availability is now coupled to Inventory's availability — if Inventory is slow or down, every order-placement request hangs or fails too, unless you deliberately design against it.
@CircuitBreaker(name = "inventoryService", fallbackMethod = "assumeOutOfStock") public boolean hasStock(Long productId, int quantity) { return restClient.get().uri("/inventory/{id}", productId) .retrieve().body(StockResponse.class).available() >= quantity; } private boolean assumeOutOfStock(Long productId, int quantity, Throwable t) { log.warn("inventory service unreachable, failing safe: {}", t.getMessage()); return false; // fail safe: don't oversell when we can't verify stock }
◆ Under the hood — what a circuit breaker actually does
A circuit breaker tracks recent failure rate to a downstream dependency; after too many failures, it "opens" and stops even attempting new calls for a cooldown period, immediately routing to the fallback instead — protecting the failing downstream service from being hammered by retries while it's already struggling, and protecting the calling service from piling up slow, doomed requests waiting on a timeout.
✓ Quick recap
What's the main reason to route all client traffic through a gateway instead of directly to each service? Clients only need to know one address, and cross-cutting concerns (auth, rate limiting, logging) live in one place instead of duplicated per service. What problem does a circuit breaker solve that a plain try/catch doesn't? It stops attempting calls to a struggling dependency entirely for a cooldown period, rather than retrying (and piling up slow failures) indefinitely.
💻 Code example
@CircuitBreaker(name = "inventoryService", fallbackMethod = "assumeOutOfStock") public boolean hasStock(Long productId, int quantity) { return restClient.get().uri("/inventory/{id}", productId) .retrieve().body(StockResponse.class).available() >= quantity; } private boolean assumeOutOfStock(Long productId, int quantity, Throwable t) { log.warn("inventory service unreachable, failing safe: {}", t.getMessage()); return false; // fail safe: don't oversell when we can't verify stock }
Want a visual for this concept?
Generate a diagram tailored to “API Gateway & Service Communication” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.
Sign in to generate a visual →