advanced~2h

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 responsibilityWhy it belongs here, not in each service
RoutingOne place mapping /orders/** → Order service, /inventory/** → Inventory service.
AuthenticationVerify the JWT (Module 13) once at the gateway, rather than duplicating token-validation logic in every downstream service.
Rate limitingOne consistent policy across the whole system, instead of per-service reimplementation.
Cross-cutting logging/tracingA 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 →

Practice quiz

Next Step

Continue to Docker & Docker Compose← Back to all Spring Boot chapters