āļŠāļąāļĄāļ āļēāļĐāļ“āđŒ Spring Cloud Gateway: Routing, Filter āđāļĨāļ° Load Balancing

āđ€āļŠāļĩāđˆāļĒāļ§āļŠāļēāļ Spring Cloud Gateway āļŠāļģāļŦāļĢāļąāļšāļāļēāļĢāļŠāļąāļĄāļ āļēāļĐāļ“āđŒāđ€āļ—āļ„āļ™āļīāļ„: 12 āļ„āļģāļ–āļēāļĄāļ„āļĢāļ­āļšāļ„āļĨāļļāļĄ routing, filter, load balancing āđāļĨāļ° pattern API Gateway āļžāļĢāđ‰āļ­āļĄāļ•āļąāļ§āļ­āļĒāđˆāļēāļ‡āđ‚āļ„āđ‰āļ”.

Spring Cloud Gateway: āļ„āļģāļ–āļēāļĄāļŠāļąāļĄāļ āļēāļĐāļ“āđŒāđ€āļĢāļ·āđˆāļ­āļ‡ routing, filter āđāļĨāļ° load balancing

Spring Cloud Gateway āļ„āļ·āļ­āđ‚āļ‹āļĨāļđāļŠāļąāļ™āļ•āđ‰āļ™āđāļšāļšāļŠāļģāļŦāļĢāļąāļšāļāļēāļĢāļ™āļģ API Gateway āļĄāļēāđƒāļŠāđ‰āļ‡āļēāļ™āđƒāļ™āļŠāļ–āļēāļ›āļąāļ•āļĒāļāļĢāļĢāļĄāđ„āļĄāđ‚āļ„āļĢāđ€āļ‹āļ­āļĢāđŒāļ§āļīāļŠāļ‚āļ­āļ‡ Spring āļāļēāļĢāļŠāļąāļĄāļ āļēāļĐāļ“āđŒāđ€āļ—āļ„āļ™āļīāļ„āļˆāļ°āļ›āļĢāļ°āđ€āļĄāļīāļ™āļ„āļ§āļēāļĄāļŠāļēāļĄāļēāļĢāļ–āđƒāļ™āļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļē routing āļŠāļĢāđ‰āļēāļ‡ filter āļ—āļĩāđˆāļāļģāļŦāļ™āļ”āđ€āļ­āļ‡ āđāļĨāļ°āļˆāļąāļ”āļāļēāļĢ load balancing āļ­āļĒāđˆāļēāļ‡āļĄāļĩāļ›āļĢāļ°āļŠāļīāļ—āļ˜āļīāļ āļēāļž

āļ„āļģāđāļ™āļ°āļ™āļģāđƒāļ™āļāļēāļĢāđ€āļ•āļĢāļĩāļĒāļĄāļ•āļąāļ§

āļœāļđāđ‰āļŠāļĢāļĢāļŦāļēāļšāļļāļ„āļĨāļēāļāļĢāļˆāļ°āļ•āļĢāļ§āļˆāļŠāļ­āļšāļ„āļ§āļēāļĄāđ€āļ‚āđ‰āļēāđƒāļˆāđƒāļ™āļĢāļđāļ›āđāļšāļš Gateway: āļāļēāļĢāļĒāļ·āļ™āļĒāļąāļ™āļ•āļąāļ§āļ•āļ™āđāļšāļšāļĢāļ§āļĄāļĻāļđāļ™āļĒāđŒ rate limiting āđāļĨāļ° circuit breaker āļ„āļ§āļēāļĄāļŠāļēāļĄāļēāļĢāļ–āđƒāļ™āļāļēāļĢāļ­āļ˜āļīāļšāļēāļĒāļ§āđˆāļēāļ—āļģāđ„āļĄāļˆāļķāļ‡āđ€āļĨāļ·āļ­āļ Spring Cloud Gateway āđāļ—āļ™āļ—āļēāļ‡āđ€āļĨāļ·āļ­āļāļ­āļ·āđˆāļ™āđ€āļ›āđ‡āļ™āļˆāļļāļ”āļ—āļĩāđˆāļŠāļĢāđ‰āļēāļ‡āļ„āļ§āļēāļĄāđāļ•āļāļ•āđˆāļēāļ‡

āļŠāļ–āļēāļ›āļąāļ•āļĒāļāļĢāļĢāļĄāđāļĨāļ°āļžāļ·āđ‰āļ™āļāļēāļ™āļ‚āļ­āļ‡ Spring Cloud Gateway

āļ„āļģāļ–āļēāļĄāļ—āļĩāđˆ 1: Spring Cloud Gateway āļ„āļ·āļ­āļ­āļ°āđ„āļĢāđāļĨāļ°āļ—āļģāđ„āļĄāļˆāļķāļ‡āļ„āļ§āļĢāđƒāļŠāđ‰?

Spring Cloud Gateway āđ€āļ›āđ‡āļ™ API Gateway āđāļšāļš reactive āļ—āļĩāđˆāļŠāļĢāđ‰āļēāļ‡āļšāļ™ Spring WebFlux āđāļĨāļ° Project Reactor āļ—āļģāļŦāļ™āđ‰āļēāļ—āļĩāđˆāđ€āļ›āđ‡āļ™āļˆāļļāļ”āđ€āļ‚āđ‰āļēāđ€āļ”āļĩāļĒāļ§āļŠāļģāļŦāļĢāļąāļšāļ„āļģāļ‚āļ­āļ—āļąāđ‰āļ‡āļŦāļĄāļ”āļ—āļĩāđˆāļŠāđˆāļ‡āđ„āļ›āļĒāļąāļ‡āđ„āļĄāđ‚āļ„āļĢāđ€āļ‹āļ­āļĢāđŒāļ§āļīāļŠ āđ‚āļ”āļĒāđƒāļŦāđ‰āļ„āļ§āļēāļĄāļŠāļēāļĄāļēāļĢāļ– routing āļāļēāļĢāļāļĢāļ­āļ‡ āđāļĨāļ° load balancing

GatewayApplication.javajava
// āļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļēāļžāļ·āđ‰āļ™āļāļēāļ™āļ‚āļ­āļ‡ Spring Cloud Gateway
@SpringBootApplication
public class GatewayApplication {

    public static void main(String[] args) {
        // āđ€āļĢāļīāđˆāļĄāļ•āđ‰āļ™āđ€āļ‹āļīāļĢāđŒāļŸāđ€āļ§āļ­āļĢāđŒ reactive Netty (āđ„āļĄāđˆāđƒāļŠāđˆ Tomcat)
        SpringApplication.run(GatewayApplication.class, args);
    }
}
yaml
# application.yml
# āļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļē gateway āļ‚āļąāđ‰āļ™āļ•āđˆāļģ
spring:
  cloud:
    gateway:
      routes:
        # āđ€āļŠāđ‰āļ™āļ—āļēāļ‡āđ„āļ›āļĒāļąāļ‡āđ€āļ‹āļ­āļĢāđŒāļ§āļīāļŠāļœāļđāđ‰āđƒāļŠāđ‰
        - id: user-service
          uri: http://localhost:8081
          predicates:
            - Path=/api/users/**
        # āđ€āļŠāđ‰āļ™āļ—āļēāļ‡āđ„āļ›āļĒāļąāļ‡āđ€āļ‹āļ­āļĢāđŒāļ§āļīāļŠāļ„āļģāļŠāļąāđˆāļ‡āļ‹āļ·āđ‰āļ­
        - id: order-service
          uri: http://localhost:8082
          predicates:
            - Path=/api/orders/**

āļ‚āđ‰āļ­āļ”āļĩāļŦāļĨāļąāļāļ‚āļ­āļ‡ Spring Cloud Gateway āđ„āļ”āđ‰āđāļāđˆ āļŠāļ–āļēāļ›āļąāļ•āļĒāļāļĢāļĢāļĄāđāļšāļš non-blocking āđ€āļžāļ·āđˆāļ­āļ›āļĢāļ°āļŠāļīāļ—āļ˜āļīāļ āļēāļžāļŠāļđāļ‡ āļāļēāļĢāļœāļŠāļēāļ™āļĢāļ§āļĄāļāļąāļšāļĢāļ°āļšāļšāļ™āļīāđ€āļ§āļĻ Spring Cloud āļ­āļĒāđˆāļēāļ‡āđ€āļ›āđ‡āļ™āļ˜āļĢāļĢāļĄāļŠāļēāļ•āļī āđāļĨāļ°āļĢāļ­āļ‡āļĢāļąāļš pattern reactive āļŠāļĄāļąāļĒāđƒāļŦāļĄāđˆ

āļ„āļģāļ–āļēāļĄāļ—āļĩāđˆ 2: āļ­āļ˜āļīāļšāļēāļĒāđāļ™āļ§āļ„āļīāļ” Route, Predicate āđāļĨāļ° Filter

āļŠāļēāļĄāđāļ™āļ§āļ„āļīāļ”āļŦāļĨāļąāļāļ—āļĩāđˆāļ›āļĢāļ°āļāļ­āļšāđ€āļ›āđ‡āļ™ Spring Cloud Gateway: Route āļāļģāļŦāļ™āļ”āļ›āļĨāļēāļĒāļ—āļēāļ‡ Predicate āļĢāļ°āļšāļļāļ§āđˆāļēāđ€āļĄāļ·āđˆāļ­āđƒāļ”āļ„āļ§āļĢāđƒāļŠāđ‰ route āđāļĨāļ° Filter āļ›āļĢāļąāļšāđ€āļ›āļĨāļĩāđˆāļĒāļ™āļ„āļģāļ‚āļ­āđāļĨāļ°āļāļēāļĢāļ•āļ­āļšāļāļĨāļąāļš

RouteConfiguration.javajava
// āļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļē route āđāļšāļšāđ‚āļ›āļĢāđāļāļĢāļĄ
@Configuration
public class RouteConfiguration {

    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
            // Route āļ—āļĩāđˆāļĄāļĩāļŦāļĨāļēāļĒ predicate
            .route("product-service", r -> r
                // Predicate: āđ€āļŠāđ‰āļ™āļ—āļēāļ‡ URL
                .path("/api/products/**")
                // Predicate: HTTP method
                .and()
                .method(HttpMethod.GET, HttpMethod.POST)
                // Predicate: header āļ—āļĩāđˆāļĄāļĩāļ­āļĒāļđāđˆ
                .and()
                .header("X-Api-Version", "v2")
                // Filter: āļāļēāļĢāđ€āļ‚āļĩāļĒāļ™ path āđƒāļŦāļĄāđˆ
                .filters(f -> f
                    .rewritePath("/api/products/(?<segment>.*)", "/products/${segment}")
                    // Filter: āđ€āļžāļīāđˆāļĄ header
                    .addRequestHeader("X-Gateway-Source", "spring-cloud-gateway")
                )
                // URI āļ›āļĨāļēāļĒāļ—āļēāļ‡
                .uri("http://localhost:8083"))
            .build();
    }
}

āļĨāļģāļ”āļąāļšāļāļēāļĢāļ›āļĢāļ°āļĄāļ§āļĨāļœāļĨāđ€āļ›āđ‡āļ™āđ„āļ›āļ”āļąāļ‡āļ™āļĩāđ‰:

text
āļ„āļģāļ‚āļ­āđ€āļ‚āđ‰āļē
    │
    ▾
┌─────────────────┐
│   Predicates    │ → āļ›āļĢāļ°āđ€āļĄāļīāļ™āđ€āļ‡āļ·āđˆāļ­āļ™āđ„āļ‚ (path, method, header...)
└────────┮────────┘
         │ āļžāļšāļāļēāļĢāļˆāļąāļšāļ„āļđāđˆ
         ▾
┌─────────────────┐
│  Pre-Filters    │ → āļ›āļĢāļąāļšāļ„āļģāļ‚āļ­āļāđˆāļ­āļ™ routing
└────────┮────────┘
         │
         ▾
┌─────────────────┐
│   HTTP Proxy    │ → āļŠāđˆāļ‡āļ•āđˆāļ­āđ„āļ›āļĒāļąāļ‡āđ€āļ‹āļ­āļĢāđŒāļ§āļīāļŠāļ›āļĨāļēāļĒāļ—āļēāļ‡
└────────┮────────┘
         │
         ▾
┌─────────────────┐
│  Post-Filters   │ → āļ›āļĢāļąāļšāļāļēāļĢāļ•āļ­āļšāļāļĨāļąāļšāļāđˆāļ­āļ™āļŠāđˆāļ‡āļ„āļ·āļ™āđ„āļ„āļĨāđ€āļ­āļ™āļ•āđŒ
└────────┮────────┘
         │
         ▾
   āļ•āļ­āļšāļāļĨāļąāļšāđ„āļ›āļĒāļąāļ‡āđ„āļ„āļĨāđ€āļ­āļ™āļ•āđŒ

āļ„āļģāļ–āļēāļĄāļ—āļĩāđˆ 3: predicate āļ—āļĩāđˆāđƒāļŠāđ‰āļšāđˆāļ­āļĒāļĄāļĩāļ­āļ°āđ„āļĢāļšāđ‰āļēāļ‡?

Spring Cloud Gateway āđƒāļŦāđ‰ predicate āļŠāļģāđ€āļĢāđ‡āļˆāļĢāļđāļ›āļˆāļģāļ™āļ§āļ™āļĄāļēāļāļŠāļģāļŦāļĢāļąāļšāđ€āļ‡āļ·āđˆāļ­āļ™āđ„āļ‚ routing āļ—āļĩāđˆāļŦāļĨāļēāļāļŦāļĨāļēāļĒ āļāļēāļĢāļĢāļ§āļĄ predicate āļŦāļĨāļēāļĒāļ•āļąāļ§āļŠāđˆāļ§āļĒāļŠāļĢāđ‰āļēāļ‡āļāļŽ routing āļ—āļĩāđˆāļ‹āļąāļšāļ‹āđ‰āļ­āļ™

yaml
# application.yml
# āļ•āļąāļ§āļ­āļĒāđˆāļēāļ‡ predicate āļ—āļąāđˆāļ§āđ„āļ›
spring:
  cloud:
    gateway:
      routes:
        # Routing āļ•āļēāļĄ path āļžāļĢāđ‰āļ­āļĄāļˆāļąāļšāļ•āļąāļ§āđāļ›āļĢ
        - id: user-details
          uri: http://user-service
          predicates:
            - Path=/users/{userId}

        # Routing āļ•āļēāļĄ HTTP method
        - id: user-create
          uri: http://user-service
          predicates:
            - Path=/users
            - Method=POST

        # Routing āļ•āļēāļĄ header
        - id: mobile-api
          uri: http://mobile-service
          predicates:
            - Header=X-Client-Type, mobile

        # Routing āļ•āļēāļĄ query parameter
        - id: search-api
          uri: http://search-service
          predicates:
            - Query=q

        # Routing āļ•āļēāļĄ host
        - id: admin-portal
          uri: http://admin-service
          predicates:
            - Host=admin.example.com

        # Routing āļ•āļēāļĄāđ€āļ§āļĨāļē
        - id: maintenance-mode
          uri: http://maintenance-service
          predicates:
            - Between=2026-03-20T02:00:00Z,2026-03-20T04:00:00Z
CustomPredicateFactory.javajava
// āļāļēāļĢāļŠāļĢāđ‰āļēāļ‡ predicate āļāļģāļŦāļ™āļ”āđ€āļ­āļ‡
@Component
public class ApiKeyRoutePredicateFactory
    extends AbstractRoutePredicateFactory<ApiKeyRoutePredicateFactory.Config> {

    public ApiKeyRoutePredicateFactory() {
        super(Config.class);
    }

    @Override
    public Predicate<ServerWebExchange> apply(Config config) {
        return exchange -> {
            // āļ•āļĢāļ§āļˆāļŠāļ­āļšāļāļēāļĢāļĄāļĩāļ­āļĒāļđāđˆāđāļĨāļ°āļ„āļ§āļēāļĄāļ–āļđāļāļ•āđ‰āļ­āļ‡āļ‚āļ­āļ‡ API key
            String apiKey = exchange.getRequest()
                .getHeaders()
                .getFirst("X-Api-Key");

            return apiKey != null && config.getValidKeys().contains(apiKey);
        };
    }

    @Validated
    public static class Config {
        private List<String> validKeys = new ArrayList<>();

        public List<String> getValidKeys() {
            return validKeys;
        }

        public void setValidKeys(List<String> validKeys) {
            this.validKeys = validKeys;
        }
    }
}
āļĨāļģāļ”āļąāļšāļ‚āļ­āļ‡ predicate

āļĨāļģāļ”āļąāļšāļ‚āļ­āļ‡ predicate āđ„āļĄāđˆāļŠāđˆāļ‡āļœāļĨāļ•āđˆāļ­āļāļēāļĢāļ›āļĢāļ°āđ€āļĄāļīāļ™ āđāļ•āđˆāļĨāļģāļ”āļąāļšāļ‚āļ­āļ‡ route āļŠāļģāļ„āļąāļ Route āļˆāļ°āļ–āļđāļāļ›āļĢāļ°āđ€āļĄāļīāļ™āļ•āļēāļĄāļĨāļģāļ”āļąāļšāđāļĨāļ°āļˆāļ°āđƒāļŠāđ‰āļ„āļđāđˆāļ—āļĩāđˆāļ•āļĢāļ‡āļāļąāļ™āļ„āļđāđˆāđāļĢāļ

Filter āđāļĨāļ°āļāļēāļĢāđāļ›āļĨāļ‡āļ„āļģāļ‚āļ­

āļ„āļģāļ–āļēāļĄāļ—āļĩāđˆ 4: filter āļāđˆāļ­āļ™āđāļĨāļ°āļŦāļĨāļąāļ‡āļāļēāļĢāļ›āļĢāļ°āļĄāļ§āļĨāļœāļĨāļ—āļģāļ‡āļēāļ™āļ­āļĒāđˆāļēāļ‡āđ„āļĢ?

Filter GatewayFilter āļ—āļģāļ‡āļēāļ™āđƒāļ™āļŦāđˆāļ§āļ‡āđ‚āļ‹āđˆāļ—āļĩāđˆāļĄāļĩāļĨāļģāļ”āļąāļš filter "pre" āļˆāļ°āļ›āļĢāļąāļšāļ„āļģāļ‚āļ­āļāđˆāļ­āļ™ routing āļŠāđˆāļ§āļ™ filter "post" āļˆāļ°āļ›āļĢāļąāļšāļāļēāļĢāļ•āļ­āļšāļāļĨāļąāļšāļŦāļĨāļąāļ‡āļˆāļēāļāđ„āļ”āđ‰āļĢāļąāļšāļˆāļēāļāđ€āļ‹āļ­āļĢāđŒāļ§āļīāļŠāļ›āļĨāļēāļĒāļ—āļēāļ‡

LoggingFilter.javajava
// Filter logging āđāļšāļšāđ‚āļāļĨāļšāļ­āļĨ
@Component
@Slf4j
public class LoggingFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // PRE-FILTER: āļāđˆāļ­āļ™ routing
        String requestId = UUID.randomUUID().toString();
        long startTime = System.currentTimeMillis();

        log.info("Request {} started: {} {}",
            requestId,
            exchange.getRequest().getMethod(),
            exchange.getRequest().getPath());

        // āđ€āļžāļīāđˆāļĄ request ID āđ€āļ‚āđ‰āļēāđƒāļ™ header
        ServerHttpRequest modifiedRequest = exchange.getRequest()
            .mutate()
            .header("X-Request-Id", requestId)
            .build();

        // āļ”āļģāđ€āļ™āļīāļ™āļŦāđˆāļ§āļ‡āđ‚āļ‹āđˆāļ•āđˆāļ­āđāļĨāļ°āļˆāļąāļ”āļāļēāļĢāļāļēāļĢāļ•āļ­āļšāļāļĨāļąāļš
        return chain.filter(exchange.mutate().request(modifiedRequest).build())
            .then(Mono.fromRunnable(() -> {
                // POST-FILTER: āļŦāļĨāļąāļ‡āļāļēāļĢāļ•āļ­āļšāļāļĨāļąāļš
                long duration = System.currentTimeMillis() - startTime;
                HttpStatusCode status = exchange.getResponse().getStatusCode();

                log.info("Request {} completed: status={}, duration={}ms",
                    requestId, status, duration);
            }));
    }

    @Override
    public int getOrder() {
        // āļĨāļģāļ”āļąāļšāļ•āļīāļ”āļĨāļš = āļ—āļģāļ‡āļēāļ™āļāđˆāļ­āļ™
        return -1;
    }
}
AuthenticationFilter.javajava
// Filter āļĒāļ·āļ™āļĒāļąāļ™āļ•āļąāļ§āļ•āļ™ JWT
@Component
@RequiredArgsConstructor
public class AuthenticationFilter implements GatewayFilter {

    private final JwtTokenValidator tokenValidator;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String authHeader = exchange.getRequest()
            .getHeaders()
            .getFirst(HttpHeaders.AUTHORIZATION);

        // āļ•āļĢāļ§āļˆāļŠāļ­āļšāļāļēāļĢāļĄāļĩāļ­āļĒāļđāđˆāļ‚āļ­āļ‡ token
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            return handleUnauthorized(exchange, "Missing or invalid Authorization header");
        }

        String token = authHeader.substring(7);

        // āļ•āļĢāļ§āļˆāļŠāļ­āļš token āđāļšāļš reactive
        return tokenValidator.validate(token)
            .flatMap(claims -> {
                // āđ€āļŠāļĢāļīāļĄāļ‚āđ‰āļ­āļĄāļđāļĨāļœāļđāđ‰āđƒāļŠāđ‰āđƒāļŦāđ‰āļāļąāļšāļ„āļģāļ‚āļ­
                ServerHttpRequest enrichedRequest = exchange.getRequest()
                    .mutate()
                    .header("X-User-Id", claims.getSubject())
                    .header("X-User-Roles", String.join(",", claims.getRoles()))
                    .build();

                return chain.filter(exchange.mutate().request(enrichedRequest).build());
            })
            .onErrorResume(e -> handleUnauthorized(exchange, e.getMessage()));
    }

    private Mono<Void> handleUnauthorized(ServerWebExchange exchange, String message) {
        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
        exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);

        String body = String.format("{\"error\": \"%s\"}", message);
        DataBuffer buffer = exchange.getResponse()
            .bufferFactory()
            .wrap(body.getBytes(StandardCharsets.UTF_8));

        return exchange.getResponse().writeWith(Mono.just(buffer));
    }
}

āļ„āļģāļ–āļēāļĄāļ—āļĩāđˆ 5: filter āļŠāļģāđ€āļĢāđ‡āļˆāļĢāļđāļ›āļ—āļĩāđˆāļĄāļĩāļ›āļĢāļ°āđ‚āļĒāļŠāļ™āđŒāļ—āļĩāđˆāļŠāļļāļ”āļĄāļĩāļ­āļ°āđ„āļĢāļšāđ‰āļēāļ‡?

Spring Cloud Gateway āļĄāļĩ filter āļŠāļģāđ€āļĢāđ‡āļˆāļĢāļđāļ›āļ„āļĢāļ­āļšāļ„āļĨāļļāļĄāļāļĢāļ“āļĩāđƒāļŠāđ‰āļ‡āļēāļ™āļ—āļąāđˆāļ§āđ„āļ›: āļāļēāļĢāđ€āļ‚āļĩāļĒāļ™ URL āđƒāļŦāļĄāđˆ āļāļēāļĢāļ›āļĢāļąāļš header āļāļēāļĢ retry āđāļĨāļ° circuit breaker

yaml
# application.yml
# Filter āļŠāļģāđ€āļĢāđ‡āļˆāļĢāļđāļ›āļ—āļĩāđˆāđƒāļŠāđ‰āļšāđˆāļ­āļĒ
spring:
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/orders/**
          filters:
            # āļāļēāļĢāđ€āļ‚āļĩāļĒāļ™ path āđƒāļŦāļĄāđˆ
            - RewritePath=/api/orders/(?<segment>.*), /orders/${segment}

            # āđ€āļžāļīāđˆāļĄ header āļ‚āļ­āļ‡āļ„āļģāļ‚āļ­
            - AddRequestHeader=X-Gateway-Version, 1.0

            # āļĨāļš header āļ•āļ­āļšāļāļĨāļąāļšāļ—āļĩāđˆāļ­āđˆāļ­āļ™āđ„āļŦāļ§
            - RemoveResponseHeader=X-Powered-By
            - RemoveResponseHeader=Server

            # āļ„āļģāļ™āļģāļŦāļ™āđ‰āļē path
            - PrefixPath=/v2

            # āļĨāļšāļ„āļģāļ™āļģāļŦāļ™āđ‰āļē
            - StripPrefix=1

            # Retry āļ­āļąāļ•āđ‚āļ™āļĄāļąāļ•āļīāđ€āļĄāļ·āđˆāļ­āđ€āļāļīāļ”āļ‚āđ‰āļ­āļœāļīāļ”āļžāļĨāļēāļ”
            - name: Retry
              args:
                retries: 3
                statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE
                methods: GET
                backoff:
                  firstBackoff: 100ms
                  maxBackoff: 500ms
                  factor: 2

            # āļāļēāļĢāļˆāļģāļāļąāļ”āļ­āļąāļ•āļĢāļē
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10
                redis-rate-limiter.burstCapacity: 20
                key-resolver: "#{@userKeyResolver}"
RateLimiterConfiguration.javajava
// āļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļē rate limiter āļ•āđˆāļ­āļœāļđāđ‰āđƒāļŠāđ‰
@Configuration
public class RateLimiterConfiguration {

    @Bean
    public KeyResolver userKeyResolver() {
        // āļˆāļģāļāļąāļ”āļ•āđˆāļ­āļœāļđāđ‰āđƒāļŠāđ‰āļ—āļĩāđˆāļĒāļ·āļ™āļĒāļąāļ™āļ•āļąāļ§āļ•āļ™āđāļĨāđ‰āļ§
        return exchange -> Mono.just(
            exchange.getRequest()
                .getHeaders()
                .getFirst("X-User-Id")
        ).defaultIfEmpty("anonymous");
    }

    @Bean
    public KeyResolver ipKeyResolver() {
        // āļˆāļģāļāļąāļ”āļ•āđˆāļ­āļ—āļĩāđˆāļ­āļĒāļđāđˆ IP
        return exchange -> Mono.just(
            Objects.requireNonNull(exchange.getRequest()
                .getRemoteAddress())
                .getAddress()
                .getHostAddress()
        );
    }
}

āļžāļĢāđ‰āļ­āļĄāļ—āļĩāđˆāļˆāļ°āļžāļīāļŠāļīāļ•āļāļēāļĢāļŠāļąāļĄāļ āļēāļĐāļ“āđŒ Spring Boot āđāļĨāđ‰āļ§āļŦāļĢāļ·āļ­āļĒāļąāļ‡āļ„āļĢāļąāļš?

āļāļķāļāļāļ™āļ”āđ‰āļ§āļĒāļ•āļąāļ§āļˆāļģāļĨāļ­āļ‡āđāļšāļšāđ‚āļ•āđ‰āļ•āļ­āļš, flashcards āđāļĨāļ°āđāļšāļšāļ—āļ”āļŠāļ­āļšāđ€āļ—āļ„āļ™āļīāļ„āļ„āļĢāļąāļš

āļ„āļģāļ–āļēāļĄāļ—āļĩāđˆ 6: āļˆāļ°āļ›āļĢāļąāļš body āļ”āđ‰āļ§āļĒ filter āđ„āļ”āđ‰āļ­āļĒāđˆāļēāļ‡āđ„āļĢ?

āļāļēāļĢāļ›āļĢāļąāļš body āļ‚āļ­āļ‡āļ„āļģāļ‚āļ­āļŦāļĢāļ·āļ­āļāļēāļĢāļ•āļ­āļšāļāļĨāļąāļšāļ•āđ‰āļ­āļ‡āđƒāļŠāđ‰āđāļ™āļ§āļ—āļēāļ‡āđ€āļ‰āļžāļēāļ°āļāļąāļš ModifyRequestBodyGatewayFilterFactory āļŦāļĢāļ·āļ­ ModifyResponseBodyGatewayFilterFactory

RequestBodyModificationFilter.javajava
// Filter āļ›āļĢāļąāļš body āļ‚āļ­āļ‡āļ„āļģāļ‚āļ­
@Component
@RequiredArgsConstructor
public class RequestBodyModificationFilter implements GlobalFilter, Ordered {

    private final ObjectMapper objectMapper;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // āļ›āļĢāļąāļšāđ€āļ‰āļžāļēāļ°āļ„āļģāļ‚āļ­ POST/PUT āļ—āļĩāđˆāđ€āļ›āđ‡āļ™ JSON
        if (!isJsonRequest(exchange)) {
            return chain.filter(exchange);
        }

        return DataBufferUtils.join(exchange.getRequest().getBody())
            .flatMap(dataBuffer -> {
                byte[] bytes = new byte[dataBuffer.readableByteCount()];
                dataBuffer.read(bytes);
                DataBufferUtils.release(dataBuffer);

                try {
                    // Parse āđāļĨāļ°āļ›āļĢāļąāļš JSON
                    Map<String, Object> body = objectMapper.readValue(
                        bytes,
                        new TypeReference<Map<String, Object>>() {}
                    );

                    // āđ€āļžāļīāđˆāļĄāđ€āļĄāļ•āļēāļ”āļēāļ•āđ‰āļē
                    body.put("processedAt", Instant.now().toString());
                    body.put("gatewayVersion", "1.0");

                    byte[] modifiedBytes = objectMapper.writeValueAsBytes(body);

                    // āļŠāļĢāđ‰āļēāļ‡āļ„āļģāļ‚āļ­āđƒāļŦāļĄāđˆāļ”āđ‰āļ§āļĒ body āļ—āļĩāđˆāļ›āļĢāļąāļšāđāļĨāđ‰āļ§
                    ServerHttpRequest modifiedRequest = new ServerHttpRequestDecorator(
                        exchange.getRequest()
                    ) {
                        @Override
                        public Flux<DataBuffer> getBody() {
                            return Flux.just(
                                exchange.getResponse()
                                    .bufferFactory()
                                    .wrap(modifiedBytes)
                            );
                        }

                        @Override
                        public HttpHeaders getHeaders() {
                            HttpHeaders headers = new HttpHeaders();
                            headers.putAll(super.getHeaders());
                            headers.setContentLength(modifiedBytes.length);
                            return headers;
                        }
                    };

                    return chain.filter(exchange.mutate().request(modifiedRequest).build());

                } catch (IOException e) {
                    return Mono.error(new RuntimeException("Failed to parse request body", e));
                }
            });
    }

    private boolean isJsonRequest(ServerWebExchange exchange) {
        MediaType contentType = exchange.getRequest().getHeaders().getContentType();
        return contentType != null &&
               contentType.isCompatibleWith(MediaType.APPLICATION_JSON);
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
}
ResponseBodyModificationConfig.javajava
// āļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļēāđ€āļžāļ·āđˆāļ­āļ›āļĢāļąāļšāļāļēāļĢāļ•āļ­āļšāļāļĨāļąāļš
@Configuration
public class ResponseBodyModificationConfig {

    @Bean
    public RouteLocator responseModifyingRoutes(
        RouteLocatorBuilder builder,
        ModifyResponseBodyGatewayFilterFactory modifyResponseBodyFilter
    ) {
        return builder.routes()
            .route("modify-response", r -> r
                .path("/api/users/**")
                .filters(f -> f.modifyResponseBody(
                    String.class,
                    String.class,
                    MediaType.APPLICATION_JSON_VALUE,
                    (exchange, responseBody) -> {
                        // āļŦāļļāđ‰āļĄāļāļēāļĢāļ•āļ­āļšāļāļĨāļąāļšāđƒāļ™āļĢāļđāļ›āđāļšāļšāļĄāļēāļ•āļĢāļāļēāļ™
                        return Mono.just(String.format(
                            "{\"success\": true, \"data\": %s, \"timestamp\": \"%s\"}",
                            responseBody,
                            Instant.now()
                        ));
                    }
                ))
                .uri("lb://user-service"))
            .build();
    }
}

Load Balancing āđāļĨāļ°āļ„āļ§āļēāļĄāļ—āļ™āļ—āļēāļ™

āļ„āļģāļ–āļēāļĄāļ—āļĩāđˆ 7: āļ•āļąāđ‰āļ‡āļ„āđˆāļē load balancing āļ”āđ‰āļ§āļĒ Spring Cloud LoadBalancer āļ­āļĒāđˆāļēāļ‡āđ„āļĢ?

Spring Cloud Gateway āļœāļŠāļēāļ™āļāļąāļš Spring Cloud LoadBalancer āđ€āļžāļ·āđˆāļ­āļāļĢāļ°āļˆāļēāļĒāļ—āļĢāļēāļŸāļŸāļīāļāļĢāļ°āļŦāļ§āđˆāļēāļ‡āļ­āļīāļ™āļŠāđāļ•āļ™āļ‹āđŒāļ‚āļ­āļ‡āđ€āļ‹āļ­āļĢāđŒāļ§āļīāļŠ āļĢāļđāļ›āđāļšāļš URI lb:// āđ€āļ›āļīāļ”āđƒāļŠāđ‰āļ‡āļēāļ™ load balancing āļ­āļąāļ•āđ‚āļ™āļĄāļąāļ•āļī

yaml
# application.yml
# āļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļē load balancing
spring:
  cloud:
    gateway:
      routes:
        - id: user-service
          # lb:// āđ€āļ›āļīāļ”āđƒāļŠāđ‰āļ‡āļēāļ™ load balancing
          uri: lb://user-service
          predicates:
            - Path=/api/users/**

    # āļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļē load balancer
    loadbalancer:
      ribbon:
        enabled: false  # āđƒāļŠāđ‰ Spring Cloud LoadBalancer (āđ„āļĄāđˆāđƒāļŠāđˆ Ribbon)

      # āļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļēāļ•āļēāļĄāđ€āļ‹āļ­āļĢāđŒāļ§āļīāļŠ
      configurations: default

      # Health check āļŠāļģāļŦāļĢāļąāļš load balancing
      health-check:
        path:
          user-service: /actuator/health
        interval: 10s

# Service discovery (Eureka āļŦāļĢāļ·āļ­āļ­āļ·āđˆāļ™āđ†)
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
LoadBalancerConfiguration.javajava
// āļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļē load balancer āđāļšāļšāļāļģāļŦāļ™āļ”āđ€āļ­āļ‡
@Configuration
@LoadBalancerClients(defaultConfiguration = CustomLoadBalancerConfig.class)
public class LoadBalancerConfiguration {
}

// CustomLoadBalancerConfig.java
// āļāļĨāļĒāļļāļ—āļ˜āđŒ load balancing āđāļšāļšāļāļģāļŦāļ™āļ”āđ€āļ­āļ‡
public class CustomLoadBalancerConfig {

    @Bean
    public ReactorLoadBalancer<ServiceInstance> loadBalancer(
        Environment environment,
        LoadBalancerClientFactory clientFactory
    ) {
        String serviceId = environment.getProperty(
            LoadBalancerClientFactory.PROPERTY_NAME
        );

        // āđƒāļŠāđ‰ Round Robin āđ€āļ›āđ‡āļ™āļ„āđˆāļēāđ€āļĢāļīāđˆāļĄāļ•āđ‰āļ™
        return new RoundRobinLoadBalancer(
            clientFactory.getLazyProvider(serviceId, ServiceInstanceListSupplier.class),
            serviceId
        );
    }

    @Bean
    public ServiceInstanceListSupplier serviceInstanceListSupplier(
        ConfigurableApplicationContext context
    ) {
        // āđ€āļžāļīāđˆāļĄ health check āđƒāļŦāđ‰āļāļąāļšāļ­āļīāļ™āļŠāđāļ•āļ™āļ‹āđŒ
        return ServiceInstanceListSupplier.builder()
            .withDiscoveryClient()
            .withHealthChecks()
            .withCaching()
            .build(context);
    }
}
WeightedLoadBalancer.javajava
// Load balancer āļ•āļēāļĄāļ™āđ‰āļģāļŦāļ™āļąāļ
@Component
@RequiredArgsConstructor
public class WeightedLoadBalancer implements ReactorServiceInstanceLoadBalancer {

    private final String serviceId;
    private final ObjectProvider<ServiceInstanceListSupplier> supplierProvider;
    private final Random random = new Random();

    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        return supplierProvider.getIfAvailable()
            .get()
            .next()
            .map(instances -> {
                if (instances.isEmpty()) {
                    return new EmptyResponse();
                }

                // āļ„āļģāļ™āļ§āļ“āļ™āđ‰āļģāļŦāļ™āļąāļāļˆāļēāļāđ€āļĄāļ•āļēāļ”āļēāļ•āđ‰āļē
                List<WeightedInstance> weighted = instances.stream()
                    .map(instance -> {
                        int weight = Integer.parseInt(
                            instance.getMetadata().getOrDefault("weight", "1")
                        );
                        return new WeightedInstance(instance, weight);
                    })
                    .toList();

                // āļāļēāļĢāđ€āļĨāļ·āļ­āļāļ•āļēāļĄāļ™āđ‰āļģāļŦāļ™āļąāļ
                int totalWeight = weighted.stream()
                    .mapToInt(WeightedInstance::weight)
                    .sum();

                int randomWeight = random.nextInt(totalWeight);
                int currentWeight = 0;

                for (WeightedInstance wi : weighted) {
                    currentWeight += wi.weight();
                    if (randomWeight < currentWeight) {
                        return new DefaultResponse(wi.instance());
                    }
                }

                return new DefaultResponse(weighted.get(0).instance());
            });
    }

    private record WeightedInstance(ServiceInstance instance, int weight) {}
}

āļ„āļģāļ–āļēāļĄāļ—āļĩāđˆ 8: āļ™āļģ circuit breaker āļĄāļēāđƒāļŠāđ‰āđƒāļ™ gateway āļ­āļĒāđˆāļēāļ‡āđ„āļĢ?

Circuit breaker āļ›āđ‰āļ­āļ‡āļāļąāļ™āļ„āļ§āļēāļĄāļĨāđ‰āļĄāđ€āļŦāļĨāļ§āļ•āđˆāļ­āđ€āļ™āļ·āđˆāļ­āļ‡āđ€āļ›āđ‡āļ™āļĨāļđāļāđ‚āļ‹āđˆ Spring Cloud Gateway āļœāļŠāļēāļ™āļāļąāļš Resilience4j āļŠāļģāļŦāļĢāļąāļšāļāļēāļĢāļˆāļąāļ”āļāļēāļĢāļ„āļ§āļēāļĄāļĨāđ‰āļĄāđ€āļŦāļĨāļ§āļ‚āļąāđ‰āļ™āļŠāļđāļ‡

yaml
# application.yml
# āļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļē Circuit Breaker āļ‚āļ­āļ‡ Resilience4j
spring:
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/orders/**
          filters:
            # Circuit breaker āļžāļĢāđ‰āļ­āļĄ fallback
            - name: CircuitBreaker
              args:
                name: orderServiceCB
                fallbackUri: forward:/fallback/orders

resilience4j:
  circuitbreaker:
    configs:
      default:
        # āļˆāļģāļ™āļ§āļ™āļ„āļģāļ‚āļ­āļ—āļĩāđˆāļ›āļĢāļ°āđ€āļĄāļīāļ™
        slidingWindowSize: 10
        # āđ€āļāļ“āļ‘āđŒāļ„āļ§āļēāļĄāļĨāđ‰āļĄāđ€āļŦāļĨāļ§āđ€āļžāļ·āđˆāļ­āđ€āļ›āļīāļ”āļ§āļ‡āļˆāļĢ
        failureRateThreshold: 50
        # āļĢāļ°āļĒāļ°āđ€āļ§āļĨāļēāļ—āļĩāđˆāļ§āļ‡āļˆāļĢāđ€āļ›āļīāļ”āļ­āļĒāļđāđˆāļāđˆāļ­āļ™āļĨāļ­āļ‡āđƒāļŦāļĄāđˆ
        waitDurationInOpenState: 30s
        # āļ„āļģāļ‚āļ­āļ—āļĩāđˆāļ­āļ™āļļāļāļēāļ•āđƒāļ™āļŠāļ–āļēāļ™āļ° half-open
        permittedNumberOfCallsInHalfOpenState: 3
        # āļāļēāļĢāđ€āļ›āļĨāļĩāđˆāļĒāļ™āļŠāļ–āļēāļ™āļ°āļ­āļąāļ•āđ‚āļ™āļĄāļąāļ•āļī
        automaticTransitionFromOpenToHalfOpenEnabled: true

    instances:
      orderServiceCB:
        baseConfig: default
        failureRateThreshold: 60

  timelimiter:
    configs:
      default:
        timeoutDuration: 5s

    instances:
      orderServiceCB:
        timeoutDuration: 3s
FallbackController.javajava
// Controller āļŠāļģāļĢāļ­āļ‡
@RestController
@RequestMapping("/fallback")
@Slf4j
public class FallbackController {

    @GetMapping("/orders")
    public Mono<ResponseEntity<Map<String, Object>>> ordersFallback(
        ServerWebExchange exchange
    ) {
        log.warn("Circuit breaker activated for orders service");

        Map<String, Object> response = Map.of(
            "success", false,
            "error", "Service temporarily unavailable",
            "code", "SERVICE_UNAVAILABLE",
            "retryAfter", 30
        );

        return Mono.just(ResponseEntity
            .status(HttpStatus.SERVICE_UNAVAILABLE)
            .body(response));
    }

    @PostMapping("/orders")
    public Mono<ResponseEntity<Map<String, Object>>> ordersPostFallback() {
        Map<String, Object> response = Map.of(
            "success", false,
            "error", "Order creation temporarily unavailable",
            "code", "SERVICE_UNAVAILABLE",
            "message", "Please try again later"
        );

        return Mono.just(ResponseEntity
            .status(HttpStatus.SERVICE_UNAVAILABLE)
            .body(response));
    }
}
CircuitBreakerEventListener.javajava
// āļāļēāļĢāļ•āļīāļ”āļ•āļēāļĄāđ€āļŦāļ•āļļāļāļēāļĢāļ“āđŒāļ‚āļ­āļ‡ circuit breaker
@Component
@Slf4j
@RequiredArgsConstructor
public class CircuitBreakerEventListener {

    private final MeterRegistry meterRegistry;

    @EventListener
    public void onCircuitBreakerStateTransition(
        CircuitBreakerOnStateTransitionEvent event
    ) {
        CircuitBreaker.StateTransition transition = event.getStateTransition();

        log.info("Circuit breaker {} state changed: {} -> {}",
            event.getCircuitBreakerName(),
            transition.getFromState(),
            transition.getToState());

        // āđ€āļĄāļ•āļĢāļīāļ Micrometer
        meterRegistry.counter(
            "circuit_breaker.state_transition",
            "name", event.getCircuitBreakerName(),
            "from", transition.getFromState().name(),
            "to", transition.getToState().name()
        ).increment();
    }

    @EventListener
    public void onCircuitBreakerFailure(CircuitBreakerOnErrorEvent event) {
        log.error("Circuit breaker {} error: {}",
            event.getCircuitBreakerName(),
            event.getThrowable().getMessage());
    }
}
Timeout āđāļĨāļ° circuit breaker

āļ•āļąāđ‰āļ‡ timeout āđƒāļŦāđ‰āļŠāļ­āļ”āļ„āļĨāđ‰āļ­āļ‡āļāļąāļ™āļĢāļ°āļŦāļ§āđˆāļēāļ‡ circuit breaker āđāļĨāļ° HTTP client timeout āļ—āļĩāđˆāļ™āļēāļ™āđ€āļāļīāļ™āđ„āļ›āļˆāļ°āļšāļĨāđ‡āļ­āļ thread āļŠāđˆāļ§āļ™āļ—āļĩāđˆāļŠāļąāđ‰āļ™āđ€āļāļīāļ™āđ„āļ›āļ—āļģāđƒāļŦāđ‰āđ€āļāļīāļ”āļŠāļąāļāļāļēāļ“āđ€āļ•āļ·āļ­āļ™āļœāļīāļ”āļžāļĨāļēāļ”

āļ„āļģāļ–āļēāļĄāļ—āļĩāđˆ 9: āļ—āļģ retry āļ”āđ‰āļ§āļĒ exponential backoff āļ­āļĒāđˆāļēāļ‡āđ„āļĢ?

Retry āđāļšāļšāļ‰āļĨāļēāļ”āļ”āđ‰āļ§āļĒ exponential backoff āļŠāđˆāļ§āļĒāđ„āļĄāđˆāđƒāļŦāđ‰āđ€āļ‹āļ­āļĢāđŒāļ§āļīāļŠāļ—āļĩāđˆāļāļģāļĨāļąāļ‡āļĄāļĩāļ›āļąāļāļŦāļēāļ–āļđāļāļ–āļĨāđˆāļĄāđ€āļžāļīāđˆāļĄ āļ‚āļ“āļ°āđ€āļ”āļĩāļĒāļ§āļāļąāļ™āļāđ‡āđ€āļžāļīāđˆāļĄāđ‚āļ­āļāļēāļŠāļŠāļģāđ€āļĢāđ‡āļˆāđƒāļŦāđ‰āļŠāļđāļ‡āļŠāļļāļ”

yaml
# application.yml
# āļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļē retry āļžāļĢāđ‰āļ­āļĄ backoff
spring:
  cloud:
    gateway:
      routes:
        - id: payment-service
          uri: lb://payment-service
          predicates:
            - Path=/api/payments/**
          filters:
            - name: Retry
              args:
                # āļˆāļģāļ™āļ§āļ™āļ„āļĢāļąāđ‰āļ‡āļ—āļĩāđˆāļžāļĒāļēāļĒāļēāļĄ
                retries: 3
                # āļĢāļŦāļąāļŠ HTTP āļ—āļĩāđˆāļāļĢāļ°āļ•āļļāđ‰āļ™ retry
                statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE,GATEWAY_TIMEOUT
                # āđ€āļ‰āļžāļēāļ° HTTP method āļ—āļĩāđˆ idempotent
                methods: GET,PUT
                # Exception āļ—āļĩāđˆāļāļĢāļ°āļ•āļļāđ‰āļ™ retry
                exceptions:
                  - java.io.IOException
                  - java.net.ConnectException
                  - org.springframework.cloud.gateway.support.TimeoutException
                # Exponential backoff
                backoff:
                  firstBackoff: 100ms
                  maxBackoff: 2000ms
                  factor: 2
                  basedOnPreviousValue: true
CustomRetryFilter.javajava
// Retry āđāļšāļšāļāļģāļŦāļ™āļ”āđ€āļ­āļ‡āļžāļĢāđ‰āļ­āļĄāļ•āļĢāļĢāļāļ°āļ˜āļļāļĢāļāļīāļˆ
@Component
@Slf4j
public class CustomRetryFilter implements GatewayFilter, Ordered {

    private static final int MAX_RETRIES = 3;
    private static final Duration INITIAL_BACKOFF = Duration.ofMillis(100);
    private static final double BACKOFF_MULTIPLIER = 2.0;
    private static final double JITTER_FACTOR = 0.1;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        return Mono.defer(() -> attemptRequest(exchange, chain, 0));
    }

    private Mono<Void> attemptRequest(
        ServerWebExchange exchange,
        GatewayFilterChain chain,
        int attempt
    ) {
        return chain.filter(exchange)
            .onErrorResume(throwable -> {
                if (attempt >= MAX_RETRIES || !isRetryable(throwable, exchange)) {
                    return Mono.error(throwable);
                }

                Duration backoff = calculateBackoff(attempt);
                log.warn("Retry attempt {} after {}ms for {} {}",
                    attempt + 1,
                    backoff.toMillis(),
                    exchange.getRequest().getMethod(),
                    exchange.getRequest().getPath());

                return Mono.delay(backoff)
                    .then(Mono.defer(() ->
                        attemptRequest(exchange, chain, attempt + 1)
                    ));
            });
    }

    private boolean isRetryable(Throwable throwable, ServerWebExchange exchange) {
        // Retry āđ€āļ‰āļžāļēāļ° method āļ—āļĩāđˆ idempotent
        HttpMethod method = exchange.getRequest().getMethod();
        if (!Set.of(HttpMethod.GET, HttpMethod.PUT, HttpMethod.DELETE)
            .contains(method)) {
            return false;
        }

        // āļ•āļĢāļ§āļˆāļŠāļ­āļšāļ›āļĢāļ°āđ€āļ āļ—āļ‚āļ­āļ‡ exception
        return throwable instanceof ConnectException ||
               throwable instanceof TimeoutException ||
               throwable instanceof ServiceUnavailableException;
    }

    private Duration calculateBackoff(int attempt) {
        // Exponential backoff āļžāļĢāđ‰āļ­āļĄ jitter
        long baseBackoff = (long) (
            INITIAL_BACKOFF.toMillis() * Math.pow(BACKOFF_MULTIPLIER, attempt)
        );

        // āđ€āļžāļīāđˆāļĄ jitter āđāļšāļšāļŠāļļāđˆāļĄ (Âą10%)
        double jitter = 1.0 + (Math.random() - 0.5) * 2 * JITTER_FACTOR;
        long finalBackoff = (long) (baseBackoff * jitter);

        return Duration.ofMillis(finalBackoff);
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE - 1;
    }
}

āļĢāļđāļ›āđāļšāļšāļ‚āļąāđ‰āļ™āļŠāļđāļ‡āđāļĨāļ°āđāļ™āļ§āļ›āļāļīāļšāļąāļ•āļīāļ—āļĩāđˆāļ”āļĩ

āļ„āļģāļ–āļēāļĄāļ—āļĩāđˆ 10: āļ—āļģ aggregation āļ‚āļ­āļ‡āļ„āļģāļ‚āļ­āļ­āļĒāđˆāļēāļ‡āđ„āļĢ?

Aggregation āļĢāļ§āļĄāļāļēāļĢāđ€āļĢāļĩāļĒāļāđ„āļĄāđ‚āļ„āļĢāđ€āļ‹āļ­āļĢāđŒāļ§āļīāļŠāļŦāļĨāļēāļĒāļ„āļĢāļąāđ‰āļ‡āđƒāļŦāđ‰āđ€āļ›āđ‡āļ™āļāļēāļĢāļ•āļ­āļšāļāļĨāļąāļšāđ€āļ”āļĩāļĒāļ§āļŠāļģāļŦāļĢāļąāļšāđ„āļ„āļĨāđ€āļ­āļ™āļ•āđŒ āļĨāļ”āļ„āļ§āļēāļĄāļŦāļ™āđˆāļ§āļ‡āđāļĨāļ°āļ„āļ§āļēāļĄāļ‹āļąāļšāļ‹āđ‰āļ­āļ™āļ‚āļ­āļ‡āļāļąāđˆāļ‡ frontend

AggregationController.javajava
// Aggregation āļŦāļĨāļēāļĒāđ€āļ‹āļ­āļĢāđŒāļ§āļīāļŠ
@RestController
@RequestMapping("/api/aggregate")
@RequiredArgsConstructor
@Slf4j
public class AggregationController {

    private final WebClient.Builder webClientBuilder;
    private final CircuitBreakerRegistry circuitBreakerRegistry;

    @GetMapping("/user-dashboard/{userId}")
    public Mono<DashboardResponse> getUserDashboard(@PathVariable Long userId) {
        // āđ€āļĢāļĩāļĒāļāđāļšāļšāļ‚āļ™āļēāļ™āđ„āļ›āļĒāļąāļ‡āđ€āļ‹āļ­āļĢāđŒāļ§āļīāļŠāļ•āđˆāļēāļ‡āđ†
        Mono<UserProfile> userMono = fetchUserProfile(userId);
        Mono<List<Order>> ordersMono = fetchRecentOrders(userId);
        Mono<NotificationCount> notificationsMono = fetchNotificationCount(userId);

        // āļĢāļ§āļĄāļœāļĨāļĨāļąāļžāļ˜āđŒ
        return Mono.zip(userMono, ordersMono, notificationsMono)
            .map(tuple -> DashboardResponse.builder()
                .user(tuple.getT1())
                .recentOrders(tuple.getT2())
                .notificationCount(tuple.getT3())
                .generatedAt(Instant.now())
                .build())
            .timeout(Duration.ofSeconds(5))
            .onErrorResume(this::handleAggregationError);
    }

    private Mono<UserProfile> fetchUserProfile(Long userId) {
        return webClientBuilder.build()
            .get()
            .uri("lb://user-service/users/{id}", userId)
            .retrieve()
            .bodyToMono(UserProfile.class)
            .transform(CircuitBreakerOperator.of(
                circuitBreakerRegistry.circuitBreaker("user-service")
            ))
            .onErrorReturn(new UserProfile(userId, "Unknown", null));
    }

    private Mono<List<Order>> fetchRecentOrders(Long userId) {
        return webClientBuilder.build()
            .get()
            .uri("lb://order-service/orders?userId={id}&limit=5", userId)
            .retrieve()
            .bodyToFlux(Order.class)
            .collectList()
            .transform(CircuitBreakerOperator.of(
                circuitBreakerRegistry.circuitBreaker("order-service")
            ))
            .onErrorReturn(Collections.emptyList());
    }

    private Mono<NotificationCount> fetchNotificationCount(Long userId) {
        return webClientBuilder.build()
            .get()
            .uri("lb://notification-service/notifications/count/{id}", userId)
            .retrieve()
            .bodyToMono(NotificationCount.class)
            .transform(CircuitBreakerOperator.of(
                circuitBreakerRegistry.circuitBreaker("notification-service")
            ))
            .onErrorReturn(new NotificationCount(0, 0));
    }

    private Mono<DashboardResponse> handleAggregationError(Throwable error) {
        log.error("Dashboard aggregation failed: {}", error.getMessage());
        return Mono.just(DashboardResponse.builder()
            .error("Partial data available")
            .generatedAt(Instant.now())
            .build());
    }
}
DashboardResponse.javajava
// DTO āļ‚āļ­āļ‡āļāļēāļĢāļ•āļ­āļšāļāļĨāļąāļšāļ—āļĩāđˆāļĢāļ§āļĄāđāļĨāđ‰āļ§
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DashboardResponse {
    private UserProfile user;
    private List<Order> recentOrders;
    private NotificationCount notificationCount;
    private Instant generatedAt;
    private String error;
}

āļ„āļģāļ–āļēāļĄāļ—āļĩāđˆ 11: āļ—āļģāđƒāļŦāđ‰ gateway āļ›āļĨāļ­āļ”āļ āļąāļĒāļ”āđ‰āļ§āļĒ OAuth2 āļ­āļĒāđˆāļēāļ‡āđ„āļĢ?

āļāļēāļĢāļœāļŠāļēāļ™ OAuth2 āļĢāļ§āļĄāļĻāļđāļ™āļĒāđŒāļāļēāļĢāļĒāļ·āļ™āļĒāļąāļ™āļ•āļąāļ§āļ•āļ™āļ—āļĩāđˆāļĢāļ°āļ”āļąāļš gateway āļŦāļĨāļĩāļāđ€āļĨāļĩāđˆāļĒāļ‡āļāļēāļĢāļ—āļģāļ‹āđ‰āļģāļ•āļĢāļĢāļāļ°āđƒāļ™āđāļ•āđˆāļĨāļ°āđ„āļĄāđ‚āļ„āļĢāđ€āļ‹āļ­āļĢāđŒāļ§āļīāļŠ

yaml
# application.yml
# āļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļē OAuth2 Resource Server
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://auth.example.com/realms/myrealm
          jwk-set-uri: https://auth.example.com/realms/myrealm/protocol/openid-connect/certs
SecurityConfig.javajava
// āļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļēāļ„āļ§āļēāļĄāļ›āļĨāļ­āļ”āļ āļąāļĒāļ‚āļ­āļ‡ gateway
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {

    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
        return http
            // āļ›āļīāļ” CSRF āļŠāļģāļŦāļĢāļąāļš API āđāļšāļšāđ„āļĢāđ‰āļŠāļ–āļēāļ™āļ°
            .csrf(ServerHttpSecurity.CsrfSpec::disable)

            // āļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļēāļāļēāļĢāļ­āļ™āļļāļāļēāļ•
            .authorizeExchange(exchanges -> exchanges
                // Endpoint āļŠāļēāļ˜āļēāļĢāļ“āļ°
                .pathMatchers("/actuator/health", "/actuator/info").permitAll()
                .pathMatchers("/api/public/**").permitAll()
                .pathMatchers("/api/auth/**").permitAll()

                // Endpoint āļ•āļēāļĄāļšāļ—āļšāļēāļ—
                .pathMatchers("/api/admin/**").hasRole("ADMIN")
                .pathMatchers(HttpMethod.DELETE, "/api/**").hasRole("ADMIN")

                // āļŠāđˆāļ§āļ™āļ—āļĩāđˆāđ€āļŦāļĨāļ·āļ­āļ•āđ‰āļ­āļ‡āļĒāļ·āļ™āļĒāļąāļ™āļ•āļąāļ§āļ•āļ™
                .anyExchange().authenticated()
            )

            // OAuth2 Resource Server āļžāļĢāđ‰āļ­āļĄ JWT
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .jwtAuthenticationConverter(jwtAuthenticationConverter())
                )
            )

            .build();
    }

    @Bean
    public ReactiveJwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter =
            new JwtGrantedAuthoritiesConverter();
        // āļ”āļķāļ‡āļšāļ—āļšāļēāļ—āļˆāļēāļ claim "roles"
        grantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
        grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");

        ReactiveJwtAuthenticationConverter converter =
            new ReactiveJwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(
            new ReactiveJwtGrantedAuthoritiesConverterAdapter(grantedAuthoritiesConverter)
        );

        return converter;
    }
}
TokenRelayFilter.javajava
// āļāļēāļĢāļŠāđˆāļ‡āļ•āđˆāļ­ token āđ„āļ›āļĒāļąāļ‡āđ€āļ‹āļ­āļĢāđŒāļ§āļīāļŠāļ›āļĨāļēāļĒāļ™āđ‰āļģ
@Component
public class TokenRelayFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        return ReactiveSecurityContextHolder.getContext()
            .map(SecurityContext::getAuthentication)
            .filter(auth -> auth instanceof JwtAuthenticationToken)
            .cast(JwtAuthenticationToken.class)
            .map(JwtAuthenticationToken::getToken)
            .map(jwt -> {
                // āļŠāđˆāļ‡āļ•āđˆāļ­ token āđ„āļ›āļĒāļąāļ‡āđ€āļ‹āļ­āļĢāđŒāļ§āļīāļŠāļ›āļĨāļēāļĒāļ™āđ‰āļģ
                ServerHttpRequest request = exchange.getRequest()
                    .mutate()
                    .header(HttpHeaders.AUTHORIZATION, "Bearer " + jwt.getTokenValue())
                    // āđ€āļžāļīāđˆāļĄāļ‚āđ‰āļ­āļĄāļđāļĨāļœāļđāđ‰āđƒāļŠāđ‰āļ—āļĩāđˆāļ”āļķāļ‡āļˆāļēāļ token
                    .header("X-User-Id", jwt.getSubject())
                    .header("X-User-Email", jwt.getClaimAsString("email"))
                    .build();

                return exchange.mutate().request(request).build();
            })
            .defaultIfEmpty(exchange)
            .flatMap(chain::filter);
    }

    @Override
    public int getOrder() {
        // āļŦāļĨāļąāļ‡āļāļēāļĢāļĒāļ·āļ™āļĒāļąāļ™āļ•āļąāļ§āļ•āļ™ āļāđˆāļ­āļ™ routing
        return SecurityWebFiltersOrder.AUTHENTICATION.getOrder() + 1;
    }
}

āļžāļĢāđ‰āļ­āļĄāļ—āļĩāđˆāļˆāļ°āļžāļīāļŠāļīāļ•āļāļēāļĢāļŠāļąāļĄāļ āļēāļĐāļ“āđŒ Spring Boot āđāļĨāđ‰āļ§āļŦāļĢāļ·āļ­āļĒāļąāļ‡āļ„āļĢāļąāļš?

āļāļķāļāļāļ™āļ”āđ‰āļ§āļĒāļ•āļąāļ§āļˆāļģāļĨāļ­āļ‡āđāļšāļšāđ‚āļ•āđ‰āļ•āļ­āļš, flashcards āđāļĨāļ°āđāļšāļšāļ—āļ”āļŠāļ­āļšāđ€āļ—āļ„āļ™āļīāļ„āļ„āļĢāļąāļš

āļ„āļģāļ–āļēāļĄāļ—āļĩāđˆ 12: āđāļ™āļ§āļ›āļāļīāļšāļąāļ•āļīāļ—āļĩāđˆāļ”āļĩāđƒāļ™āļāļēāļĢāļĄāļ­āļ™āļīāđ€āļ•āļ­āļĢāđŒāđāļĨāļ° observability āļĄāļĩāļ­āļ°āđ„āļĢāļšāđ‰āļēāļ‡?

āļāļēāļĢāļĄāļ­āļ™āļīāđ€āļ•āļ­āļĢāđŒ gateway āļŠāļģāļ„āļąāļāļ­āļĒāđˆāļēāļ‡āļĒāļīāđˆāļ‡āļ•āđˆāļ­āļāļēāļĢāļĢāļ°āļšāļļāļ›āļąāļāļŦāļēāļ›āļĢāļ°āļŠāļīāļ—āļ˜āļīāļ āļēāļžāđāļĨāļ°āļ„āļ§āļēāļĄāļžāļĢāđ‰āļ­āļĄāđƒāļŠāđ‰āļ‡āļēāļ™āđƒāļ™āļŠāļ–āļēāļ›āļąāļ•āļĒāļāļĢāļĢāļĄāđ„āļĄāđ‚āļ„āļĢāđ€āļ‹āļ­āļĢāđŒāļ§āļīāļŠ

yaml
# application.yml
# āļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļē observability
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus,gateway
  metrics:
    tags:
      application: api-gateway
    distribution:
      percentiles-histogram:
        http.server.requests: true
      percentiles:
        http.server.requests: 0.5, 0.95, 0.99
  tracing:
    sampling:
      probability: 1.0

spring:
  cloud:
    gateway:
      metrics:
        enabled: true
        tags:
          path:
            enabled: true
MetricsFilter.javajava
// Filter āđ€āļĄāļ•āļĢāļīāļāđāļšāļšāļāļģāļŦāļ™āļ”āđ€āļ­āļ‡
@Component
@RequiredArgsConstructor
@Slf4j
public class MetricsFilter implements GlobalFilter, Ordered {

    private final MeterRegistry meterRegistry;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        long startTime = System.nanoTime();
        String path = exchange.getRequest().getPath().value();
        String method = exchange.getRequest().getMethod().name();

        // āļ”āļķāļ‡āđ€āļ‹āļ­āļĢāđŒāļ§āļīāļŠāļ›āļĨāļēāļĒāļ—āļēāļ‡āļˆāļēāļ route
        Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
        String routeId = route != null ? route.getId() : "unknown";

        return chain.filter(exchange)
            .doOnSuccess(v -> recordMetrics(exchange, startTime, routeId, "success"))
            .doOnError(e -> recordMetrics(exchange, startTime, routeId, "error"));
    }

    private void recordMetrics(
        ServerWebExchange exchange,
        long startTime,
        String routeId,
        String outcome
    ) {
        long duration = System.nanoTime() - startTime;
        HttpStatusCode status = exchange.getResponse().getStatusCode();
        String statusCode = status != null ? String.valueOf(status.value()) : "0";

        // Timer āļŠāļģāļŦāļĢāļąāļšāļ„āļ§āļēāļĄāļŦāļ™āđˆāļ§āļ‡
        Timer.builder("gateway.request.duration")
            .tag("route", routeId)
            .tag("method", exchange.getRequest().getMethod().name())
            .tag("status", statusCode)
            .tag("outcome", outcome)
            .register(meterRegistry)
            .record(duration, TimeUnit.NANOSECONDS);

        // āļ•āļąāļ§āļ™āļąāļšāļ„āļģāļ‚āļ­
        meterRegistry.counter(
            "gateway.requests.total",
            "route", routeId,
            "status", statusCode
        ).increment();

        // Log āļ„āļģāļ‚āļ­āļ—āļĩāđˆāļŠāđ‰āļē
        if (duration > TimeUnit.SECONDS.toNanos(1)) {
            log.warn("Slow request: {} {} - {}ms (route: {})",
                exchange.getRequest().getMethod(),
                exchange.getRequest().getPath(),
                TimeUnit.NANOSECONDS.toMillis(duration),
                routeId);
        }
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
}
TracingFilter.javajava
// āļāļēāļĢāļŠāđˆāļ‡āļ•āđˆāļ­āļšāļĢāļīāļšāļ— tracing
@Component
@RequiredArgsConstructor
public class TracingFilter implements GlobalFilter, Ordered {

    private final Tracer tracer;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // āļŠāļĢāđ‰āļēāļ‡āļŦāļĢāļ·āļ­āđ€āļĢāļĩāļĒāļ span tracing
        Span span = tracer.nextSpan()
            .name("gateway-request")
            .tag("http.method", exchange.getRequest().getMethod().name())
            .tag("http.url", exchange.getRequest().getURI().toString())
            .start();

        // āļ‰āļĩāļ” header tracing
        ServerHttpRequest request = exchange.getRequest()
            .mutate()
            .header("X-Trace-Id", span.context().traceId())
            .header("X-Span-Id", span.context().spanId())
            .build();

        return chain.filter(exchange.mutate().request(request).build())
            .doOnSuccess(v -> {
                HttpStatusCode status = exchange.getResponse().getStatusCode();
                span.tag("http.status_code",
                    status != null ? String.valueOf(status.value()) : "0");
                span.end();
            })
            .doOnError(e -> {
                span.tag("error", e.getMessage());
                span.end();
            });
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE + 1;
    }
}

āļ•āļēāļĢāļēāļ‡āđ€āļĄāļ•āļĢāļīāļāļŠāļģāļ„āļąāļ:

text
| āđ€āļĄāļ•āļĢāļīāļ                            | āļ„āļģāļ­āļ˜āļīāļšāļēāļĒ                          |
|-----------------------------------|----------------------------------|
| gateway.request.duration          | āļ„āļ§āļēāļĄāļŦāļ™āđˆāļ§āļ‡āļ•āđˆāļ­ route/āļŠāļ–āļēāļ™āļ°          |
| gateway.requests.total            | āļ•āļąāļ§āļ™āļąāļšāļ„āļģāļ‚āļ­                        |
| resilience4j.circuitbreaker.state | āļŠāļ–āļēāļ™āļ°āļ‚āļ­āļ‡ circuit breaker          |
| http.server.requests              | āđ€āļĄāļ•āļĢāļīāļ HTTP āļĄāļēāļ•āļĢāļāļēāļ™               |
| spring.cloud.gateway.routes       | route āļ—āļĩāđˆāļāļģāļĨāļąāļ‡āđƒāļŠāđ‰āļ‡āļēāļ™                |
Dashboard āļ—āļĩāđˆāđāļ™āļ°āļ™āļģ

āđƒāļŠāđ‰ Grafana āļĢāđˆāļ§āļĄāļāļąāļš dashboard āļ‚āļ­āļ‡ Spring Cloud Gateway āđāļĨāļ° Resilience4j āđ€āļžāļ·āđˆāļ­āđāļŠāļ”āļ‡āļœāļĨāđ€āļĄāļ•āļĢāļīāļ āļ•āļąāđ‰āļ‡āļāļēāļĢāđāļˆāđ‰āļ‡āđ€āļ•āļ·āļ­āļ™āļŠāļģāļŦāļĢāļąāļšāļ„āļ§āļēāļĄāļŦāļ™āđˆāļ§āļ‡ P99 āđāļĨāļ°āļ­āļąāļ•āļĢāļēāļ‚āđ‰āļ­āļœāļīāļ”āļžāļĨāļēāļ”

āļšāļ—āļŠāļĢāļļāļ›

Spring Cloud Gateway āđ€āļ›āđ‡āļ™āļ­āļ‡āļ„āđŒāļ›āļĢāļ°āļāļ­āļšāļŠāļģāļ„āļąāļāļ‚āļ­āļ‡āļŠāļ–āļēāļ›āļąāļ•āļĒāļāļĢāļĢāļĄāđ„āļĄāđ‚āļ„āļĢāđ€āļ‹āļ­āļĢāđŒāļ§āļīāļŠāļĒāļļāļ„āđƒāļŦāļĄāđˆ āļ›āļĢāļ°āđ€āļ”āđ‡āļ™āļŦāļĨāļąāļāļ—āļĩāđˆāļ„āļ§āļĢāļˆāļ”āļˆāļģāļŠāļģāļŦāļĢāļąāļšāļāļēāļĢāļŠāļąāļĄāļ āļēāļĐāļ“āđŒ:

āļŠāļ–āļēāļ›āļąāļ•āļĒāļāļĢāļĢāļĄāđāļĨāļ°āđāļ™āļ§āļ„āļīāļ”:

  • ✅ Routes, Predicates āđāļĨāļ° Filters āļ„āļ·āļ­āđ‚āļĄāđ€āļ”āļĨāļžāļ·āđ‰āļ™āļāļēāļ™
  • ✅ āļŠāļ–āļēāļ›āļąāļ•āļĒāļāļĢāļĢāļĄ reactive āļ”āđ‰āļ§āļĒ WebFlux āđāļĨāļ° Netty
  • ✅ āļœāļŠāļēāļ™āđ€āļ‚āđ‰āļēāļāļąāļšāļĢāļ°āļšāļšāļ™āļīāđ€āļ§āļĻ Spring Cloud āļ­āļĒāđˆāļēāļ‡āđ€āļ›āđ‡āļ™āļ˜āļĢāļĢāļĄāļŠāļēāļ•āļī

āļŸāļĩāđ€āļˆāļ­āļĢāđŒāļŠāļģāļ„āļąāļ:

  • ✅ Routing āđāļšāļšāđ„āļ”āļ™āļēāļĄāļīāļāļ•āļēāļĄāļŦāļĨāļēāļĒāđ€āļāļ“āļ‘āđŒ
  • ✅ Filter pre/post āļŠāļģāļŦāļĢāļąāļšāļāļēāļĢāđāļ›āļĨāļ‡āļ„āļģāļ‚āļ­āđāļĨāļ°āļāļēāļĢāļ•āļ­āļšāļāļĨāļąāļš
  • ✅ Load balancing āļ”āđ‰āļ§āļĒ Spring Cloud LoadBalancer

āļ„āļ§āļēāļĄāļ—āļ™āļ—āļēāļ™āđāļĨāļ°āļ„āļ§āļēāļĄāļ›āļĨāļ­āļ”āļ āļąāļĒ:

  • ✅ Circuit breaker āļ”āđ‰āļ§āļĒ Resilience4j āļžāļĢāđ‰āļ­āļĄ fallback
  • ✅ Retry āļ”āđ‰āļ§āļĒ exponential backoff āđāļĨāļ° jitter
  • ✅ āļāļēāļĢāļĒāļ·āļ™āļĒāļąāļ™āļ•āļąāļ§āļ•āļ™ OAuth2/JWT āđāļšāļšāļĢāļ§āļĄāļĻāļđāļ™āļĒāđŒ

Observability:

  • ✅ āđ€āļĄāļ•āļĢāļīāļ Micrometer āļžāļĢāđ‰āļ­āļĄ tag āļ•āļēāļĄ route
  • ✅ Tracing āđāļšāļšāļāļĢāļ°āļˆāļēāļĒāļžāļĢāđ‰āļ­āļĄāļāļēāļĢāļŠāđˆāļ‡āļ•āđˆāļ­āļšāļĢāļīāļšāļ—
  • ✅ Health check āđāļĨāļ° endpoint āļ‚āļ­āļ‡ actuator

āļ„āļ§āļēāļĄāđ€āļŠāļĩāđˆāļĒāļ§āļŠāļēāļāđƒāļ™ Spring Cloud Gateway āđāļŠāļ”āļ‡āļ–āļķāļ‡āļ„āļ§āļēāļĄāđ€āļ‚āđ‰āļēāđƒāļˆāļ­āļĒāđˆāļēāļ‡āļĨāļķāļāļ‹āļķāđ‰āļ‡āđƒāļ™āļĢāļđāļ›āđāļšāļšāđ„āļĄāđ‚āļ„āļĢāđ€āļ‹āļ­āļĢāđŒāļ§āļīāļŠāđāļĨāļ°āļ›āļĢāļ°āđ€āļ”āđ‡āļ™āļāļēāļĢāļ‚āļĒāļēāļĒāļĢāļ°āļšāļš āļ—āļąāļāļĐāļ°āđ€āļŦāļĨāđˆāļēāļ™āļĩāđ‰āļˆāļģāđ€āļ›āđ‡āļ™āļŠāļģāļŦāļĢāļąāļšāļāļēāļĢāļ­āļ­āļāđāļšāļš API Gateway āļ—āļĩāđˆāđāļ‚āđ‡āļ‡āđāļāļĢāđˆāļ‡āđāļĨāļ°āļ›āļĢāļ°āļŠāļīāļ—āļ˜āļīāļ āļēāļžāļŠāļđāļ‡

āđ€āļĢāļīāđˆāļĄāļāļķāļāļ‹āđ‰āļ­āļĄāđ€āļĨāļĒ!

āļ—āļ”āļŠāļ­āļšāļ„āļ§āļēāļĄāļĢāļđāđ‰āļ‚āļ­āļ‡āļ„āļļāļ“āļ”āđ‰āļ§āļĒāļ•āļąāļ§āļˆāļģāļĨāļ­āļ‡āļŠāļąāļĄāļ āļēāļĐāļ“āđŒāđāļĨāļ°āđāļšāļšāļ—āļ”āļŠāļ­āļšāđ€āļ—āļ„āļ™āļīāļ„āļ„āļĢāļąāļš

āđāļ—āđ‡āļ

#spring cloud gateway
#microservices
#api gateway
#routing
#technical interview

āđāļŠāļĢāđŒ

āļšāļ—āļ„āļ§āļēāļĄāļ—āļĩāđˆāđ€āļāļĩāđˆāļĒāļ§āļ‚āđ‰āļ­āļ‡

āļŠāļ–āļēāļ›āļąāļ•āļĒāļāļĢāļĢāļĄ event-driven āļ”āđ‰āļ§āļĒ Spring Kafka āđāļĨāļ° consumer āļ—āļĩāđˆāļ—āļ™āļ—āļēāļ™

Spring Kafka: āļŠāļ–āļēāļ›āļąāļ•āļĒāļāļĢāļĢāļĄ event-driven āļžāļĢāđ‰āļ­āļĄ consumer āļ—āļĩāđˆāļ—āļ™āļ—āļēāļ™

āļ„āļđāđˆāļĄāļ·āļ­ Spring Kafka āđāļšāļšāļ„āļĢāļšāļ–āđ‰āļ§āļ™āļŠāļģāļŦāļĢāļąāļšāļŠāļ–āļēāļ›āļąāļ•āļĒāļāļĢāļĢāļĄ event-driven āļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļē consumer āļ—āļĩāđˆāļ—āļ™āļ—āļēāļ™ āļ™āđ‚āļĒāļšāļēāļĒ retry dead letter queue āđāļĨāļ°āļĢāļđāļ›āđāļšāļšāđ‚āļ›āļĢāļ”āļąāļāļŠāļąāļ™āļŠāļģāļŦāļĢāļąāļšāđāļ­āļ›āļžāļĨāļīāđ€āļ„āļŠāļąāļ™āđāļšāļšāļāļĢāļ°āļˆāļēāļĒ

Structured logging āđƒāļ™ Spring Boot āļ”āđ‰āļ§āļĒ Logback āđāļĨāļ° JSON

Spring Boot Logging āđƒāļ™āļ›āļĩ 2026: āļĨāđ‡āļ­āļāđāļšāļšāļĄāļĩāđ‚āļ„āļĢāļ‡āļŠāļĢāđ‰āļēāļ‡āļŠāļģāļŦāļĢāļąāļšāđ‚āļ›āļĢāļ”āļąāļāļŠāļąāļ™āļ”āđ‰āļ§āļĒ Logback āđāļĨāļ° JSON

āļ„āļđāđˆāļĄāļ·āļ­āļ‰āļšāļąāļšāļŠāļĄāļšāļđāļĢāļ“āđŒāļŠāļģāļŦāļĢāļąāļš structured logging āđƒāļ™ Spring Boot āļāļēāļĢāļ•āļąāđ‰āļ‡āļ„āđˆāļē Logback JSON, MDC āļŠāļģāļŦāļĢāļąāļš tracing āđāļ™āļ§āļ›āļāļīāļšāļąāļ•āļīāļ—āļĩāđˆāļ”āļĩāļ—āļĩāđˆāļŠāļļāļ”āđƒāļ™āđ‚āļ›āļĢāļ”āļąāļāļŠāļąāļ™ āđāļĨāļ°āļāļēāļĢāļĢāļ§āļĄāļāļąāļš ELK Stack

āļāļēāļĢāļŠāļąāļĄāļ āļēāļĐāļ“āđŒāļ—āļēāļ‡āđ€āļ—āļ„āļ™āļīāļ„ Spring GraphQL āļžāļĢāđ‰āļ­āļĄ resolver āđāļĨāļ° DataLoader

āļŠāļąāļĄāļ āļēāļĐāļ“āđŒ Spring GraphQL: Resolver, DataLoader āđāļĨāļ°āļ§āļīāļ˜āļĩāđāļāđ‰āļ›āļąāļāļŦāļē N+1

āđ€āļ•āļĢāļĩāļĒāļĄāļ•āļąāļ§āļŠāļģāļŦāļĢāļąāļšāļāļēāļĢāļŠāļąāļĄāļ āļēāļĐāļ“āđŒ Spring GraphQL āļ”āđ‰āļ§āļĒāļ„āļđāđˆāļĄāļ·āļ­āļ—āļĩāđˆāļ„āļĢāļšāļ–āđ‰āļ§āļ™āļ™āļĩāđ‰ Resolver, DataLoader, āļāļēāļĢāļˆāļąāļ”āļāļēāļĢāļ›āļąāļāļŦāļē N+1, mutation āđāļĨāļ°āđāļ™āļ§āļ›āļāļīāļšāļąāļ•āļīāļ—āļĩāđˆāļ”āļĩāļ—āļĩāđˆāļŠāļļāļ”āļŠāļģāļŦāļĢāļąāļšāļ„āļģāļ–āļēāļĄāļ—āļēāļ‡āđ€āļ—āļ„āļ™āļīāļ„