Skip to content

5.3. API Gateway Usage ​

An API gateway is the shared policy layer between client traffic and backend services. Routing, auth, rate limits, observability, and protocol translation can live here; but if business rules move into the gateway, the system becomes a centralized bottleneck.

Quick Decision ​

SituationPut In GatewayKeep In Service
Authentication, TLS, rate limitsYesThe service should still not blindly trust callers
Routes, headers, correlation IDsYesThe service owns domain decisions
Business rules and data validationNoThey belong in the domain service
Network retry and circuit breakerLimitedRetry is dangerous without idempotency

Production Checklist ​

  • Problem: Which shared problem does the gateway solve, and is it more than just added technology?
  • Solution: Are route, timeout, retry, auth, and fallback behavior explicit per service?
  • Trade-off: Central policy management gets easier; bad configuration can affect all traffic.
  • Failure mode: Downstream timeout, auth failure, rate limit, and fallback responses should be distinguishable.
  • Measurement: Track gateway p95/p99 latency, upstream latency, 4xx/5xx rate, circuit state, and traffic by route.
  • Security/cost: The gateway must not be the only security layer; routing all traffic through it adds infrastructure and observability cost.

Core Functions ​

Routing ​

  • Path-based routing
  • Service discovery
  • Load balancing
  • Circuit breaking

Security ​

  • Authentication
  • Authorization
  • SSL/TLS termination
  • API key management

Monitoring ​

  • Request/response logging
  • Metrics collection
  • Performance monitoring
  • Error tracking

Kong Gateway ​

Features ​

  • Plugin architecture
  • Declarative configuration
  • Database-less mode
  • Kubernetes integration

Plugins ​

  • Rate limiting
  • JWT authentication
  • OAuth2
  • Request transformer

Deployment ​

  • Docker containers
  • Kubernetes operator
  • Hybrid mode
  • Multi-datacenter

Apigee ​

Features ​

  • API lifecycle management
  • Developer portal
  • Analytics dashboard
  • Monetization

Security ​

  • OAuth2/OpenID Connect
  • API key management
  • Threat protection
  • Data encryption

Integration ​

  • Cloud services
  • On-premise systems
  • Third-party APIs
  • Custom backends

Spring Boot API Gateway Implementation ​

Spring Cloud Gateway Configuration ​

java
@Configuration
@EnableConfigurationProperties({GatewayProperties.class})
public class ApiGatewayConfig {
    
    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
            // User Service Routes
            .route("user-service", r -> r
                .path("/api/users/**")
                .filters(f -> f
                    .stripPrefix(1)
                    .addRequestHeader("X-Gateway", "spring-cloud-gateway")
                    .circuitBreaker(config -> config
                        .setName("user-service-cb")
                        .setFallbackUri("forward:/fallback/users"))
                    .retry(config -> config
                        .setRetries(3)
                        .setBackoff(Duration.ofMillis(100), Duration.ofSeconds(1), 2, true)))
                .uri("lb://user-service"))
                
            // Order Service Routes
            .route("order-service", r -> r
                .path("/api/orders/**")
                .filters(f -> f
                    .stripPrefix(1)
                    .addRequestHeader("X-Gateway", "spring-cloud-gateway")
                    .requestRateLimiter(config -> config
                        .setRateLimiter(redisRateLimiter())
                        .setKeyResolver(userKeyResolver()))
                    .circuitBreaker(config -> config
                        .setName("order-service-cb")
                        .setFallbackUri("forward:/fallback/orders")))
                .uri("lb://order-service"))
                
            // Payment Service Routes
            .route("payment-service", r -> r
                .path("/api/payments/**")
                .filters(f -> f
                    .stripPrefix(1)
                    .addRequestHeader("X-Gateway", "spring-cloud-gateway")
                    .filter(new AuthenticationGatewayFilterFactory().apply(
                        new AuthenticationGatewayFilterFactory.Config()))
                    .requestRateLimiter(config -> config
                        .setRateLimiter(redisRateLimiter())
                        .setKeyResolver(apiKeyResolver())))
                .uri("lb://payment-service"))
                
            // Notification Service Routes
            .route("notification-service", r -> r
                .path("/api/notifications/**")
                .filters(f -> f
                    .stripPrefix(1)
                    .addRequestHeader("X-Gateway", "spring-cloud-gateway"))
                .uri("lb://notification-service"))
                
            build();
    }
    
    @Bean
    public RedisRateLimiter redisRateLimiter() {
        return new RedisRateLimiter(100, 200, 1); // replenishRate, burstCapacity, requestedTokens
    }
    
    @Bean
    public KeyResolver userKeyResolver() {
        return exchange -> {
            String userId = exchange.getRequest().getHeaders().getFirst("X-User-ID");
            return Mono.just(userId != null ? "user:" + userId : "anonymous");
        };
    }
    
    @Bean
    public KeyResolver apiKeyResolver() {
        return exchange -> {
            String apiKey = exchange.getRequest().getHeaders().getFirst("X-API-Key");
            return Mono.just(apiKey != null ? "api:" + apiKey : "anonymous");
        };
    }
    
    @Bean
    public KeyResolver ipKeyResolver() {
        return exchange -> {
            String clientIp = getClientIpAddress(exchange.getRequest());
            return Mono.just("ip:" + clientIp);
        };
    }
    
    private String getClientIpAddress(ServerHttpRequest request) {
        String xForwardedFor = request.getHeaders().getFirst("X-Forwarded-For");
        if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
            return xForwardedFor.split(",")[0].trim();
        }
        
        String xRealIp = request.getHeaders().getFirst("X-Real-IP");
        if (xRealIp != null && !xRealIp.isEmpty()) {
            return xRealIp;
        }
        
        return request.getRemoteAddress() != null ? 
            request.getRemoteAddress().getAddress().getHostAddress() : "unknown";
    }
}

Custom Authentication Filter ​

java
@Component
public class AuthenticationGatewayFilterFactory extends AbstractGatewayFilterFactory<AuthenticationGatewayFilterFactory.Config> {
    
    @Autowired
    private JwtTokenValidator jwtTokenValidator;
    
    @Autowired
    private ApiKeyValidator apiKeyValidator;
    
    public AuthenticationGatewayFilterFactory() {
        super(Config.class);
    }
    
    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            
            // Skip authentication for health checks
            if (request.getPath().value().contains("/actuator/health")) {
                return chain.filter(exchange);
            }
            
            // Try JWT authentication first
            String authHeader = request.getHeaders().getFirst("Authorization");
            if (authHeader != null && authHeader.startsWith("Bearer ")) {
                String token = authHeader.substring(7);
                return jwtTokenValidator.validateToken(token)
                    .flatMap(claims -> {
                        ServerHttpRequest mutatedRequest = request.mutate()
                            .header("X-User-ID", claims.getSubject())
                            .header("X-User-Roles", String.join(",", claims.getRoles()))
                            .build();
                        return chain.filter(exchange.mutate().request(mutatedRequest).build());
                    })
                    .onErrorResume(error -> {
                        log.warn("JWT validation failed: {}", error.getMessage());
                        return handleAuthenticationError(exchange, "Invalid JWT token");
                    });
            }
            
            // Try API Key authentication
            String apiKey = request.getHeaders().getFirst("X-API-Key");
            if (apiKey != null) {
                return apiKeyValidator.validateApiKey(apiKey)
                    .flatMap(apiKeyInfo -> {
                        ServerHttpRequest mutatedRequest = request.mutate()
                            .header("X-Client-ID", apiKeyInfo.getClientId())
                            .header("X-API-Key-Scope", String.join(",", apiKeyInfo.getScopes()))
                            .build();
                        return chain.filter(exchange.mutate().request(mutatedRequest).build());
                    })
                    .onErrorResume(error -> {
                        log.warn("API Key validation failed: {}", error.getMessage());
                        return handleAuthenticationError(exchange, "Invalid API key");
                    });
            }
            
            // No authentication provided
            return handleAuthenticationError(exchange, "Authentication required");
        };
    }
    
    private Mono<Void> handleAuthenticationError(ServerWebExchange exchange, String message) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        response.getHeaders().add("Content-Type", "application/json");
        
        String errorResponse = """
            {
                "error": "Unauthorized",
                "message": "%s",
                "timestamp": "%s"
            }
            """.formatted(message, Instant.now().toString());
        
        DataBuffer buffer = response.bufferFactory().wrap(errorResponse.getBytes());
        return response.writeWith(Mono.just(buffer));
    }
    
    @Data
    public static class Config {
        private boolean required = true;
        private List<String> excludePaths = new ArrayList<>();
    }
}

@Component
@Slf4j
public class JwtTokenValidator {
    
    @Value("${jwt.secret}")
    private String jwtSecret;
    
    @Value("${jwt.expiration:3600}")
    private long jwtExpiration;
    
    private Key getSigningKey() {
        byte[] keyBytes = Decoders.BASE64.decode(jwtSecret);
        return Keys.hmacShaKeyFor(keyBytes);
    }
    
    public Mono<JwtClaims> validateToken(String token) {
        return Mono.fromCallable(() -> {
            Claims claims = Jwts.parserBuilder()
                .setSigningKey(getSigningKey())
                .build()
                .parseClaimsJws(token)
                .getBody();
            
            List<String> roles = claims.get("roles", List.class);
            return new JwtClaims(claims.getSubject(), roles != null ? roles : new ArrayList<>());
        })
        .doOnError(error -> log.error("JWT validation error: {}", error.getMessage()))
        .onErrorMap(JwtException.class, ex -> new AuthenticationException("Invalid JWT token: " + ex.getMessage()));
    }
}

@Data
@AllArgsConstructor
public class JwtClaims {
    private String subject;
    private List<String> roles;
}

@Component
@Slf4j
public class ApiKeyValidator {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private ApiKeyRepository apiKeyRepository;
    
    public Mono<ApiKeyInfo> validateApiKey(String apiKey) {
        return Mono.fromCallable(() -> {
            // Try cache first
            String cacheKey = "api_key:" + apiKey;
            ApiKeyInfo cachedInfo = (ApiKeyInfo) redisTemplate.opsForValue().get(cacheKey);
            
            if (cachedInfo != null) {
                if (cachedInfo.isExpired()) {
                    redisTemplate.delete(cacheKey);
                    throw new AuthenticationException("API key expired");
                }
                return cachedInfo;
            }
            
            // Fallback to database
            ApiKeyEntity entity = apiKeyRepository.findByKeyHash(hashApiKey(apiKey))
                .orElseThrow(() -> new AuthenticationException("Invalid API key"));
            
            if (entity.isExpired() || !entity.isActive()) {
                throw new AuthenticationException("API key expired or inactive");
            }
            
            ApiKeyInfo info = new ApiKeyInfo(
                entity.getClientId(),
                entity.getScopes(),
                entity.getExpiresAt(),
                entity.getRateLimit()
            );
            
            // Cache for 5 minutes
            redisTemplate.opsForValue().set(cacheKey, info, Duration.ofMinutes(5));
            
            return info;
        })
        .doOnError(error -> log.error("API key validation error: {}", error.getMessage()));
    }
    
    private String hashApiKey(String apiKey) {
        return DigestUtils.sha256Hex(apiKey);
    }
}

@Data
@AllArgsConstructor
public class ApiKeyInfo implements Serializable {
    private String clientId;
    private List<String> scopes;
    private Instant expiresAt;
    private Integer rateLimit;
    
    public boolean isExpired() {
        return expiresAt != null && Instant.now().isAfter(expiresAt);
    }
}

public class AuthenticationException extends RuntimeException {
    public AuthenticationException(String message) {
        super(message);
    }
}

Circuit Breaker Configuration ​

java
@Configuration
public class CircuitBreakerConfig {
    
    @Bean
    public Customizer<ReactiveResilience4JCircuitBreakerFactory> circuitBreakerCustomizer() {
        return factory -> {
            factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
                .circuitBreakerConfig(CircuitBreakerConfiguration.ofDefaults())
                .timeLimiterConfig(TimeLimiterConfiguration.ofDefaults())
                .build());
            
            // User service specific configuration
            factory.configure(builder -> builder
                .circuitBreakerConfig(CircuitBreakerConfiguration.custom()
                    .failureRateThreshold(50)
                    .waitDurationInOpenState(Duration.ofSeconds(30))
                    .slidingWindowSize(10)
                    .minimumNumberOfCalls(5)
                    .build())
                .timeLimiterConfig(TimeLimiterConfiguration.custom()
                    .timeoutDuration(Duration.ofSeconds(5))
                    .build()), "user-service-cb");
                    
            // Order service specific configuration
            factory.configure(builder -> builder
                .circuitBreakerConfig(CircuitBreakerConfiguration.custom()
                    .failureRateThreshold(60)
                    .waitDurationInOpenState(Duration.ofSeconds(45))
                    .slidingWindowSize(15)
                    .minimumNumberOfCalls(8)
                    .build())
                .timeLimiterConfig(TimeLimiterConfiguration.custom()
                    .timeoutDuration(Duration.ofSeconds(10))
                    .build()), "order-service-cb");
        };
    }
}

Fallback Controllers ​

java
@RestController
@RequestMapping("/fallback")
@Slf4j
public class FallbackController {
    
    @GetMapping("/users/**")
    public ResponseEntity<Map<String, Object>> userServiceFallback(HttpServletRequest request) {
        log.warn("User service fallback triggered for request: {}", request.getRequestURI());
        
        Map<String, Object> fallbackResponse = Map.of(
            "error", "Service Temporarily Unavailable",
            "message", "User service is currently experiencing issues. Please try again later.",
            "service", "user-service",
            "timestamp", Instant.now(),
            "fallback", true
        );
        
        return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(fallbackResponse);
    }
    
    @GetMapping("/orders/**")
    public ResponseEntity<Map<String, Object>> orderServiceFallback(HttpServletRequest request) {
        log.warn("Order service fallback triggered for request: {}", request.getRequestURI());
        
        Map<String, Object> fallbackResponse = Map.of(
            "error", "Service Temporarily Unavailable",
            "message", "Order service is currently experiencing issues. Please try again later.",
            "service", "order-service",
            "timestamp", Instant.now(),
            "fallback", true
        );
        
        return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(fallbackResponse);
    }
    
    @PostMapping("/orders/**")
    public ResponseEntity<Map<String, Object>> orderServicePostFallback(HttpServletRequest request) {
        log.warn("Order service POST fallback triggered for request: {}", request.getRequestURI());
        
        Map<String, Object> fallbackResponse = Map.of(
            "error", "Service Temporarily Unavailable",
            "message", "Order creation is currently unavailable. Your request has been queued and will be processed when the service recovers.",
            "service", "order-service",
            "timestamp", Instant.now(),
            "fallback", true,
            "queuedForProcessing", true
        );
        
        return ResponseEntity.status(HttpStatus.ACCEPTED).body(fallbackResponse);
    }
}

Gateway Metrics and Monitoring ​

java
@Component
@Slf4j
public class GatewayMetricsFilter implements GlobalFilter, Ordered {
    
    private final MeterRegistry meterRegistry;
    private final Timer.Sample requestTimer;
    
    public GatewayMetricsFilter(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        this.requestTimer = Timer.start(meterRegistry);
    }
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        Timer.Sample sample = Timer.start(meterRegistry);
        String route = getRouteName(exchange);
        String method = exchange.getRequest().getMethod().name();
        
        return chain.filter(exchange)
            .doOnSuccess(aVoid -> recordMetrics(sample, route, method, exchange, true))
            .doOnError(error -> recordMetrics(sample, route, method, exchange, false))
            .doFinally(signalType -> {
                // Record additional metrics
                recordRequestMetrics(exchange, route, method);
            });
    }
    
    private void recordMetrics(Timer.Sample sample, String route, String method, 
                             ServerWebExchange exchange, boolean success) {
        HttpStatus status = exchange.getResponse().getStatusCode();
        
        sample.stop(Timer.builder("gateway.request.duration")
            .tag("route", route)
            .tag("method", method)
            .tag("status", status != null ? status.toString() : "unknown")
            .tag("success", String.valueOf(success))
            .register(meterRegistry));
    }
    
    private void recordRequestMetrics(ServerWebExchange exchange, String route, String method) {
        Counter.builder("gateway.requests.total")
            .tag("route", route)
            .tag("method", method)
            .register(meterRegistry)
            .increment();
            
        // Record response size
        ServerHttpResponse response = exchange.getResponse();
        if (response.getHeaders().getContentLength() > 0) {
            DistributionSummary.builder("gateway.response.size")
                .tag("route", route)
                .tag("method", method)
                .register(meterRegistry)
                .record(response.getHeaders().getContentLength());
        }
    }
    
    private String getRouteName(ServerWebExchange exchange) {
        Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
        return route != null ? route.getId() : "unknown";
    }
    
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
}

@Component
@Slf4j
public class GatewayLoggingFilter implements GlobalFilter, Ordered {
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        
        String requestId = UUID.randomUUID().toString();
        String clientIp = getClientIpAddress(request);
        String userAgent = request.getHeaders().getFirst("User-Agent");
        
        // Add request ID to headers for tracing
        ServerHttpRequest mutatedRequest = request.mutate()
            .header("X-Request-ID", requestId)
            .build();
        
        long startTime = System.currentTimeMillis();
        
        log.info("Gateway Request - ID: {}, Method: {}, URI: {}, IP: {}, User-Agent: {}",
            requestId, request.getMethod(), request.getURI(), clientIp, userAgent);
        
        return chain.filter(exchange.mutate().request(mutatedRequest).build())
            .doFinally(signalType -> {
                long duration = System.currentTimeMillis() - startTime;
                HttpStatus status = exchange.getResponse().getStatusCode();
                
                log.info("Gateway Response - ID: {}, Status: {}, Duration: {}ms",
                    requestId, status, duration);
            });
    }
    
    private String getClientIpAddress(ServerHttpRequest request) {
        String xForwardedFor = request.getHeaders().getFirst("X-Forwarded-For");
        if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
            return xForwardedFor.split(",")[0].trim();
        }
        
        String xRealIp = request.getHeaders().getFirst("X-Real-IP");
        if (xRealIp != null && !xRealIp.isEmpty()) {
            return xRealIp;
        }
        
        return request.getRemoteAddress() != null ? 
            request.getRemoteAddress().getAddress().getHostAddress() : "unknown";
    }
    
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE + 1;
    }
}

API Gateway Health Checks ​

java
@Component
public class ServiceHealthIndicator implements HealthIndicator {
    
    @Autowired
    private ReactiveLoadBalancerClientFactory loadBalancerClientFactory;
    
    @Autowired
    private WebClient.Builder webClientBuilder;
    
    private final List<String> serviceNames = Arrays.asList(
        "user-service", "order-service", "payment-service", "notification-service"
    );
    
    @Override
    public Health health() {
        Health.Builder builder = Health.up();
        Map<String, Object> details = new HashMap<>();
        
        for (String serviceName : serviceNames) {
            try {
                ServiceHealthStatus status = checkServiceHealth(serviceName);
                details.put(serviceName, status);
                
                if (!status.isHealthy()) {
                    builder.down();
                }
            } catch (Exception e) {
                details.put(serviceName, new ServiceHealthStatus(false, e.getMessage()));
                builder.down();
            }
        }
        
        return builder.withDetails(details).build();
    }
    
    private ServiceHealthStatus checkServiceHealth(String serviceName) {
        try {
            ReactiveLoadBalancerClient loadBalancerClient = 
                loadBalancerClientFactory.getInstance(serviceName);
            
            WebClient webClient = webClientBuilder.build();
            
            String healthResponse = webClient.get()
                .uri("lb://" + serviceName + "/actuator/health")
                .retrieve()
                .bodyToMono(String.class)
                .timeout(Duration.ofSeconds(5))
                .block();
                
            return new ServiceHealthStatus(true, "Service is healthy");
        } catch (Exception e) {
            log.warn("Health check failed for service {}: {}", serviceName, e.getMessage());
            return new ServiceHealthStatus(false, e.getMessage());
        }
    }
    
    @Data
    @AllArgsConstructor
    public static class ServiceHealthStatus {
        private boolean healthy;
        private String message;
    }
}

This implementation provides comprehensive Spring Cloud Gateway usage in production environments with security and scalability considerations.

Gateway Technologies and Traffic Policies ​

NeedSuitable optionsDecision note
Plugins and API lifecycleKong, Apache APISIXPlugin security and config distribution
Managed cloud gatewayAWS API Gateway, Azure API ManagementQuotas, analytics, vendor lock-in
Edge proxy/data planeEnvoy, NGINX, TraefikL4/L7, TLS, and operations model
Service-mesh ingressIstio Gateway/EnvoySeparate mesh and gateway policy

The gateway may handle authentication, rate limits, routing, request IDs, simple header/body transformation, and protocol adaptation. Keep business rules, resource authorization, and long-running aggregation out of it; use BFF and API Composition for client-specific aggregation.

Routing and Transformation ​

Path-, header-, weight-, and tenant-based routing should be released as versioned configuration alongside canary, blue-green, and A/B flows. REST-to-gRPC or HTTP-to-WebSocket translation must preserve timeout, streaming, status mapping, and cancellation semantics.

Bound request/response transformation with header allowlists, body-size limits, content types, and schema validation. A gateway-added X-User-ID header is not identity proof; downstream services must validate an access token or trusted service identity.

WAF and DDoS Layer ​

WAF filters injection, bots, abuse, and known attack signatures. For volumetric DDoS, evaluate CDN/edge provider, IP reputation, connection limits, and upstream rate limits together. Since a gateway outage can cut all traffic, document multi-instance operation, config rollback, health checks, and fail-open/fail-closed decisions.

Track route, upstream, status, p95/p99, retries, circuit state, rejected bytes, and config version.

Created by Eren Demir.