Hook: An API Gateway is the front desk of a microservices system: interviewers love it because routing, security, and failure handling all meet in one place.
Question: What is an API Gateway in Spring Boot?
Answer: An API Gateway is a single entry point in front of your microservices. In Spring Boot, it receives a client request, checks rules like authentication and path matching, then forwards the request to the right backend service or combines results from several services. It hides internal service URLs from clients and lets you centralize cross-cutting concerns like logging, rate limiting, and security.
Interview-Ready Answer: I think of an API Gateway as the front door of a microservices system. In Spring Boot, it gives me one stable URL for clients, then handles routing, auth, logging, rate limiting, and sometimes response aggregation before traffic reaches the services. The big win is simpler clients and centralized control, but I keep the gateway lightweight and highly available because it can become a bottleneck if I put business logic there.
Detailed Explanation: An API Gateway is usually a reverse proxy, meaning a server that sits in front of other servers and forwards requests on their behalf. A route predicate is the condition that decides whether a request matches a route, and a filter is small code that runs before or after the request is forwarded. In Spring Boot, real gateways are often built with Spring Cloud Gateway, which is reactive, meaning it uses non-blocking I/O so threads are not tied up while waiting for network calls.
/api/orders/123, to the gateway instead of talking to many services directly.Use an API Gateway when many clients need a stable public API but your backend is split into several services. It is especially useful when mobile, web, and partner apps need different shapes of the same data, or when you want one place for auth, throttling, TLS termination, tracing, and request normalization. It is less useful if you only have one small service, because then the extra hop adds complexity without much value.
| Tool | Main job | What it does not do |
|---|---|---|
| API Gateway | Single entry point | Heavy business logic |
| Load Balancer | Spread traffic | Auth or API shaping |
| BFF | Tailor one client | Serve all clients equally |
| Service Mesh | Service-to-service traffic | Client-facing API design |
Route matching is conceptually O(R) in the number of routes, but in practice R is usually small enough that the real cost is the extra network hop and downstream latency. Space is also roughly O(R) for route definitions plus connection pool state. In real systems, I would usually start with short user-facing timeouts like 1-3 seconds, retries only for idempotent GET calls, and a modest connection pool per downstream service such as tens to low hundreds of connections per node depending on load. A classic gotcha is making the gateway stateful or putting business rules there; that turns a thin routing layer into a monolith. Another gotcha is assuming the gateway replaces service-level security: services should still verify authorization for their own data. If you are upgrading from Spring Boot 2 to 3, remember that servlet APIs moved from javax.* to jakarta.*.
Real-World Example: Imagine an e-commerce checkout platform with web, iOS, and partner clients. All of them call one gateway endpoint, and the gateway routes checkout traffic to inventory, pricing, and order services while adding a correlation ID for tracing and checking the user’s token once at the edge. A production incident happens when a bad rewrite rule removes the /api prefix from mobile requests. Users see endless loading or 404 errors, gateway logs show route-mismatch warnings, downstream services receive almost no traffic, and the checkout team spends an hour looking in the wrong place until they inspect the gateway configuration.
package com.example.apigateway;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.server.ResponseStatusException;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@SpringBootApplication
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
// A gateway should stay thin: auth, routing, and light aggregation.
// Keeping its own worker pool small prevents one slow downstream from consuming every server thread.
@Bean(destroyMethod = "shutdown")
ExecutorService gatewayExecutor() {
return Executors.newFixedThreadPool(8);
}
@Bean
ApiKeyFilter apiKeyFilter() {
return new ApiKeyFilter("secret-123");
}
}
class ApiKeyFilter extends OncePerRequestFilter {
private final String expectedKey;
ApiKeyFilter(String expectedKey) {
this.expectedKey = expectedKey;
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
// Only protect API traffic. Internal endpoints and non-API routes are left alone.
return !request.getRequestURI().startsWith("/api/");
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String supplied = request.getHeader("X-API-Key");
// A gateway often rejects bad requests before any downstream service does work.
if (!expectedKey.equals(supplied)) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("text/plain");
response.getWriter().write("Missing or invalid X-API-Key");
return;
}
filterChain.doFilter(request, response);
}
}
record ProductView(String productId, String name, int stock, double price) { }
record GatewayResponse(String message, ProductView product) { }
@RestController
@RequestMapping("/api/products")
class ProductGatewayController {
private final InventoryService inventoryService;
private final PricingService pricingService;
private final ExecutorService gatewayExecutor;
ProductGatewayController(InventoryService inventoryService,
PricingService pricingService,
ExecutorService gatewayExecutor) {
this.inventoryService = inventoryService;
this.pricingService = pricingService;
this.gatewayExecutor = gatewayExecutor;
}
@GetMapping("/{id}")
ResponseEntity<GatewayResponse> getProduct(@PathVariable String id) {
try {
// In a real gateway, these could be HTTP calls to different services.
// Here we simulate downstream calls and show how the gateway composes them.
CompletableFuture<Integer> stockFuture = CompletableFuture
.supplyAsync(() -> inventoryService.stockFor(id), gatewayExecutor)
.orTimeout(1, TimeUnit.SECONDS);
CompletableFuture<Double> priceFuture = CompletableFuture
.supplyAsync(() -> pricingService.priceFor(id), gatewayExecutor)
.orTimeout(1, TimeUnit.SECONDS);
int stock = stockFuture.join();
double price = priceFuture.join();
return ResponseEntity.ok(
new GatewayResponse(
"Aggregated by the gateway",
new ProductView(id, "Demo Product " + id, stock, price)
)
);
} catch (CompletionException ex) {
Throwable cause = ex.getCause();
// If a downstream service deliberately said 404, pass that status through.
if (cause instanceof ResponseStatusException rse) {
throw rse;
}
// If the call exceeded the gateway timeout, clients should see 504 Gateway Timeout.
throw new ResponseStatusException(HttpStatus.GATEWAY_TIMEOUT, "A downstream service timed out", cause);
}
}
@ExceptionHandler(ResponseStatusException.class)
ResponseEntity<String> handleResponseStatusException(ResponseStatusException ex) {
String body = ex.getReason() == null ? ex.getStatusCode().toString() : ex.getReason();
return ResponseEntity.status(ex.getStatusCode()).body(body);
}
}
@Service
class InventoryService {
int stockFor(String id) {
// A real gateway must be ready for one service to reject a request while others still work.
if ("404".equals(id)) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Product not found");
}
return 42;
}
}
@Service
class PricingService {
double priceFor(String id) {
// Use this id to simulate a slow downstream dependency and prove the timeout path.
if ("timeout".equals(id)) {
slowDown(1500);
}
return 19.99;
}
private void slowDown(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "Interrupted while pricing", e);
}
}
}Follow-up & Tricky Questions:
Common Mistakes:
Memory Hook: Think of the gateway as airport security plus the information desk: it checks who you are, decides where you go, and hides the messy backstage area from travelers.
Cheat Sheet:
Practice Tasks:
/api/orders/{id} to a downstream service.X-API-Key and return 401 when it is missing.