Wawancara Spring Cloud Gateway: Routing, Filter, dan Load Balancing

Kuasai Spring Cloud Gateway untuk wawancara teknis: 12 pertanyaan tentang routing, filter, load balancing, dan pola API Gateway dengan contoh kode.

Spring Cloud Gateway: pertanyaan wawancara tentang routing, filter, dan load balancing

Spring Cloud Gateway merupakan solusi rujukan untuk menerapkan API Gateway pada arsitektur microservices Spring. Wawancara teknis menilai kemampuan untuk mengonfigurasi routing, membuat filter khusus, dan mengelola load balancing secara efektif.

Tips persiapan wawancara

Perekrut menguji pemahaman pola Gateway: autentikasi terpusat, rate limiting, dan circuit breaker. Kemampuan menjelaskan mengapa memilih Spring Cloud Gateway dibanding alternatif lain menjadi pembeda.

Arsitektur dan dasar-dasar Spring Cloud Gateway

Pertanyaan 1: Apa itu Spring Cloud Gateway dan mengapa menggunakannya?

Spring Cloud Gateway adalah API Gateway reaktif yang dibangun di atas Spring WebFlux dan Project Reactor. Komponen ini berperan sebagai titik masuk tunggal untuk seluruh permintaan ke microservices, menyediakan routing, penyaringan, dan load balancing.

GatewayApplication.javajava
// Konfigurasi dasar Spring Cloud Gateway
@SpringBootApplication
public class GatewayApplication {

    public static void main(String[] args) {
        // Memulai server reaktif Netty (bukan Tomcat)
        SpringApplication.run(GatewayApplication.class, args);
    }
}
yaml
# application.yml
# Konfigurasi minimum gateway
spring:
  cloud:
    gateway:
      routes:
        # Rute menuju layanan pengguna
        - id: user-service
          uri: http://localhost:8081
          predicates:
            - Path=/api/users/**
        # Rute menuju layanan pesanan
        - id: order-service
          uri: http://localhost:8082
          predicates:
            - Path=/api/orders/**

Keunggulan utama Spring Cloud Gateway meliputi: arsitektur non-blocking untuk performa tinggi, integrasi native dengan ekosistem Spring Cloud, dan dukungan pola reaktif modern.

Pertanyaan 2: Jelaskan konsep Route, Predicate, dan Filter

Tiga konsep dasar membentuk Spring Cloud Gateway: Route mendefinisikan tujuan, Predicate menentukan kapan rute diterapkan, dan Filter memodifikasi permintaan serta respons.

RouteConfiguration.javajava
// Konfigurasi rute secara programatik
@Configuration
public class RouteConfiguration {

    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
            // Rute dengan beberapa predicate
            .route("product-service", r -> r
                // Predicate: jalur URL
                .path("/api/products/**")
                // Predicate: metode HTTP
                .and()
                .method(HttpMethod.GET, HttpMethod.POST)
                // Predicate: header tersedia
                .and()
                .header("X-Api-Version", "v2")
                // Filter: penulisan ulang jalur
                .filters(f -> f
                    .rewritePath("/api/products/(?<segment>.*)", "/products/${segment}")
                    // Filter: tambahkan header
                    .addRequestHeader("X-Gateway-Source", "spring-cloud-gateway")
                )
                // URI tujuan
                .uri("http://localhost:8083"))
            .build();
    }
}

Alur pemrosesan mengikuti urutan berikut:

text
Permintaan masuk
┌─────────────────┐
│   Predicates    │ → Mengevaluasi syarat (path, metode, header...)
└────────┬────────┘
         │ Cocok ditemukan
┌─────────────────┐
│  Pre-Filters    │ → Memodifikasi permintaan sebelum routing
└────────┬────────┘
┌─────────────────┐
│   HTTP Proxy    │ → Meneruskan ke layanan tujuan
└────────┬────────┘
┌─────────────────┐
│  Post-Filters   │ → Memodifikasi respons sebelum dikembalikan ke klien
└────────┬────────┘
   Respons ke klien

Pertanyaan 3: Apa saja predicate yang paling sering digunakan?

Spring Cloud Gateway menyediakan banyak predicate bawaan untuk berbagai kondisi routing. Menggabungkan beberapa predicate memungkinkan aturan routing yang canggih.

yaml
# application.yml
# Contoh predicate umum
spring:
  cloud:
    gateway:
      routes:
        # Routing berdasar path dengan penangkapan variabel
        - id: user-details
          uri: http://user-service
          predicates:
            - Path=/users/{userId}

        # Routing berdasar metode HTTP
        - id: user-create
          uri: http://user-service
          predicates:
            - Path=/users
            - Method=POST

        # Routing berbasis header
        - id: mobile-api
          uri: http://mobile-service
          predicates:
            - Header=X-Client-Type, mobile

        # Routing berdasar parameter query
        - id: search-api
          uri: http://search-service
          predicates:
            - Query=q

        # Routing berbasis host
        - id: admin-portal
          uri: http://admin-service
          predicates:
            - Host=admin.example.com

        # Routing berbasis waktu
        - id: maintenance-mode
          uri: http://maintenance-service
          predicates:
            - Between=2026-03-20T02:00:00Z,2026-03-20T04:00:00Z
CustomPredicateFactory.javajava
// Pembuatan predicate khusus
@Component
public class ApiKeyRoutePredicateFactory
    extends AbstractRoutePredicateFactory<ApiKeyRoutePredicateFactory.Config> {

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

    @Override
    public Predicate<ServerWebExchange> apply(Config config) {
        return exchange -> {
            // Memeriksa keberadaan dan validitas 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;
        }
    }
}
Urutan predicate

Urutan predicate tidak memengaruhi evaluasi, tetapi urutan rute penting. Rute dievaluasi berurutan dan kecocokan pertama digunakan.

Filter dan transformasi permintaan

Pertanyaan 4: Bagaimana cara kerja filter pre dan post-processing?

Filter GatewayFilter dijalankan dalam rantai berurutan. Filter "pre" memodifikasi permintaan sebelum routing, filter "post" memodifikasi respons setelah diterima dari layanan tujuan.

LoggingFilter.javajava
// Filter logging global
@Component
@Slf4j
public class LoggingFilter implements GlobalFilter, Ordered {

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

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

        // Menambahkan ID permintaan ke header
        ServerHttpRequest modifiedRequest = exchange.getRequest()
            .mutate()
            .header("X-Request-Id", requestId)
            .build();

        // Melanjutkan rantai dan menangani respons
        return chain.filter(exchange.mutate().request(modifiedRequest).build())
            .then(Mono.fromRunnable(() -> {
                // POST-FILTER: setelah respons
                long duration = System.currentTimeMillis() - startTime;
                HttpStatusCode status = exchange.getResponse().getStatusCode();

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

    @Override
    public int getOrder() {
        // Urutan negatif = dijalankan paling awal
        return -1;
    }
}
AuthenticationFilter.javajava
// Filter autentikasi 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);

        // Memeriksa keberadaan token
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            return handleUnauthorized(exchange, "Missing or invalid Authorization header");
        }

        String token = authHeader.substring(7);

        // Memvalidasi token secara reaktif
        return tokenValidator.validate(token)
            .flatMap(claims -> {
                // Memperkaya permintaan dengan informasi pengguna
                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));
    }
}

Pertanyaan 5: Apa filter bawaan yang paling berguna?

Spring Cloud Gateway menyediakan banyak filter bawaan yang mencakup kebutuhan umum: penulisan ulang URL, modifikasi header, retry, dan circuit breaker.

yaml
# application.yml
# Filter bawaan yang umum
spring:
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/orders/**
          filters:
            # Penulisan ulang path
            - RewritePath=/api/orders/(?<segment>.*), /orders/${segment}

            # Menambahkan header permintaan
            - AddRequestHeader=X-Gateway-Version, 1.0

            # Menghapus header respons sensitif
            - RemoveResponseHeader=X-Powered-By
            - RemoveResponseHeader=Server

            # Awalan path
            - PrefixPath=/v2

            # Menghapus awalan
            - StripPrefix=1

            # Retry otomatis pada error
            - name: Retry
              args:
                retries: 3
                statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE
                methods: GET
                backoff:
                  firstBackoff: 100ms
                  maxBackoff: 500ms
                  factor: 2

            # Pembatasan laju
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10
                redis-rate-limiter.burstCapacity: 20
                key-resolver: "#{@userKeyResolver}"
RateLimiterConfiguration.javajava
// Konfigurasi rate limiter per pengguna
@Configuration
public class RateLimiterConfiguration {

    @Bean
    public KeyResolver userKeyResolver() {
        // Pembatasan per pengguna terautentikasi
        return exchange -> Mono.just(
            exchange.getRequest()
                .getHeaders()
                .getFirst("X-User-Id")
        ).defaultIfEmpty("anonymous");
    }

    @Bean
    public KeyResolver ipKeyResolver() {
        // Pembatasan per alamat IP
        return exchange -> Mono.just(
            Objects.requireNonNull(exchange.getRequest()
                .getRemoteAddress())
                .getAddress()
                .getHostAddress()
        );
    }
}

Siap menguasai wawancara Spring Boot Anda?

Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.

Pertanyaan 6: Bagaimana mengimplementasikan filter modifikasi body?

Memodifikasi body permintaan atau respons memerlukan pendekatan khusus dengan ModifyRequestBodyGatewayFilterFactory atau ModifyResponseBodyGatewayFilterFactory.

RequestBodyModificationFilter.javajava
// Filter modifikasi body permintaan
@Component
@RequiredArgsConstructor
public class RequestBodyModificationFilter implements GlobalFilter, Ordered {

    private final ObjectMapper objectMapper;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // Hanya memodifikasi permintaan POST/PUT dengan 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 {
                    // Mengurai dan memodifikasi JSON
                    Map<String, Object> body = objectMapper.readValue(
                        bytes,
                        new TypeReference<Map<String, Object>>() {}
                    );

                    // Menambahkan metadata
                    body.put("processedAt", Instant.now().toString());
                    body.put("gatewayVersion", "1.0");

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

                    // Membuat permintaan baru dengan body yang dimodifikasi
                    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
// Konfigurasi untuk memodifikasi respons
@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) -> {
                        // Membungkus respons dalam format standar
                        return Mono.just(String.format(
                            "{\"success\": true, \"data\": %s, \"timestamp\": \"%s\"}",
                            responseBody,
                            Instant.now()
                        ));
                    }
                ))
                .uri("lb://user-service"))
            .build();
    }
}

Load Balancing dan ketahanan

Pertanyaan 7: Bagaimana mengonfigurasi load balancing dengan Spring Cloud LoadBalancer?

Spring Cloud Gateway berintegrasi dengan Spring Cloud LoadBalancer untuk mendistribusikan trafik ke instance layanan. Skema URI lb:// mengaktifkan load balancing otomatis.

yaml
# application.yml
# Konfigurasi load balancing
spring:
  cloud:
    gateway:
      routes:
        - id: user-service
          # lb:// mengaktifkan load balancing
          uri: lb://user-service
          predicates:
            - Path=/api/users/**

    # Konfigurasi load balancer
    loadbalancer:
      ribbon:
        enabled: false  # Menggunakan Spring Cloud LoadBalancer (bukan Ribbon)

      # Konfigurasi per layanan
      configurations: default

      # Health check untuk load balancing
      health-check:
        path:
          user-service: /actuator/health
        interval: 10s

# Service discovery (Eureka atau lainnya)
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
LoadBalancerConfiguration.javajava
// Konfigurasi load balancer khusus
@Configuration
@LoadBalancerClients(defaultConfiguration = CustomLoadBalancerConfig.class)
public class LoadBalancerConfiguration {
}

// CustomLoadBalancerConfig.java
// Strategi load balancing khusus
public class CustomLoadBalancerConfig {

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

        // Round Robin sebagai default
        return new RoundRobinLoadBalancer(
            clientFactory.getLazyProvider(serviceId, ServiceInstanceListSupplier.class),
            serviceId
        );
    }

    @Bean
    public ServiceInstanceListSupplier serviceInstanceListSupplier(
        ConfigurableApplicationContext context
    ) {
        // Menambahkan health check pada instance
        return ServiceInstanceListSupplier.builder()
            .withDiscoveryClient()
            .withHealthChecks()
            .withCaching()
            .build(context);
    }
}
WeightedLoadBalancer.javajava
// Load balancer berbobot
@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();
                }

                // Menghitung bobot dari metadata
                List<WeightedInstance> weighted = instances.stream()
                    .map(instance -> {
                        int weight = Integer.parseInt(
                            instance.getMetadata().getOrDefault("weight", "1")
                        );
                        return new WeightedInstance(instance, weight);
                    })
                    .toList();

                // Pemilihan berbobot
                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) {}
}

Pertanyaan 8: Bagaimana menerapkan circuit breaker di gateway?

Circuit breaker melindungi dari kegagalan beruntun. Spring Cloud Gateway berintegrasi dengan Resilience4j untuk manajemen kegagalan tingkat lanjut.

yaml
# application.yml
# Konfigurasi circuit breaker Resilience4j
spring:
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/orders/**
          filters:
            # Circuit breaker dengan fallback
            - name: CircuitBreaker
              args:
                name: orderServiceCB
                fallbackUri: forward:/fallback/orders

resilience4j:
  circuitbreaker:
    configs:
      default:
        # Jumlah permintaan yang dievaluasi
        slidingWindowSize: 10
        # Ambang kegagalan untuk membuka sirkuit
        failureRateThreshold: 50
        # Durasi sirkuit terbuka sebelum mencoba lagi
        waitDurationInOpenState: 30s
        # Permintaan yang diizinkan saat half-open
        permittedNumberOfCallsInHalfOpenState: 3
        # Transisi otomatis
        automaticTransitionFromOpenToHalfOpenEnabled: true

    instances:
      orderServiceCB:
        baseConfig: default
        failureRateThreshold: 60

  timelimiter:
    configs:
      default:
        timeoutDuration: 5s

    instances:
      orderServiceCB:
        timeoutDuration: 3s
FallbackController.javajava
// Controller fallback
@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
// Pemantauan kejadian 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());

        // Metrik 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 dan circuit breaker

Konfigurasikan timeout yang konsisten antara circuit breaker dan klien HTTP. Timeout terlalu lama memblokir thread, sedangkan terlalu singkat memicu false positive.

Pertanyaan 9: Bagaimana menerapkan retry dengan backoff eksponensial?

Retry cerdas dengan backoff eksponensial mencegah membebani layanan yang sedang bermasalah, sekaligus memaksimalkan peluang sukses.

yaml
# application.yml
# Konfigurasi retry dengan backoff
spring:
  cloud:
    gateway:
      routes:
        - id: payment-service
          uri: lb://payment-service
          predicates:
            - Path=/api/payments/**
          filters:
            - name: Retry
              args:
                # Jumlah percobaan
                retries: 3
                # Kode HTTP yang memicu retry
                statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE,GATEWAY_TIMEOUT
                # Hanya metode HTTP idempoten
                methods: GET,PUT
                # Exception yang memicu retry
                exceptions:
                  - java.io.IOException
                  - java.net.ConnectException
                  - org.springframework.cloud.gateway.support.TimeoutException
                # Backoff eksponensial
                backoff:
                  firstBackoff: 100ms
                  maxBackoff: 2000ms
                  factor: 2
                  basedOnPreviousValue: true
CustomRetryFilter.javajava
// Retry kustom dengan logika bisnis
@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) {
        // Hanya mencoba ulang metode idempoten
        HttpMethod method = exchange.getRequest().getMethod();
        if (!Set.of(HttpMethod.GET, HttpMethod.PUT, HttpMethod.DELETE)
            .contains(method)) {
            return false;
        }

        // Memeriksa tipe exception
        return throwable instanceof ConnectException ||
               throwable instanceof TimeoutException ||
               throwable instanceof ServiceUnavailableException;
    }

    private Duration calculateBackoff(int attempt) {
        // Backoff eksponensial dengan jitter
        long baseBackoff = (long) (
            INITIAL_BACKOFF.toMillis() * Math.pow(BACKOFF_MULTIPLIER, attempt)
        );

        // Menambahkan jitter acak (±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;
    }
}

Pola lanjutan dan praktik terbaik

Pertanyaan 10: Bagaimana menerapkan agregasi permintaan?

Agregasi menggabungkan beberapa pemanggilan microservice menjadi satu respons untuk klien, mengurangi latensi dan kompleksitas frontend.

AggregationController.javajava
// Agregasi multi-layanan
@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) {
        // Pemanggilan paralel ke beberapa layanan
        Mono<UserProfile> userMono = fetchUserProfile(userId);
        Mono<List<Order>> ordersMono = fetchRecentOrders(userId);
        Mono<NotificationCount> notificationsMono = fetchNotificationCount(userId);

        // Agregasi hasil
        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 respons teragregasi
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DashboardResponse {
    private UserProfile user;
    private List<Order> recentOrders;
    private NotificationCount notificationCount;
    private Instant generatedAt;
    private String error;
}

Pertanyaan 11: Bagaimana mengamankan gateway dengan OAuth2?

Integrasi OAuth2 memusatkan autentikasi di tingkat gateway, menghindari duplikasi logika di setiap microservice.

yaml
# application.yml
# Konfigurasi 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
// Konfigurasi keamanan gateway
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {

    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
        return http
            // Menonaktifkan CSRF untuk API stateless
            .csrf(ServerHttpSecurity.CsrfSpec::disable)

            // Konfigurasi otorisasi
            .authorizeExchange(exchanges -> exchanges
                // Endpoint publik
                .pathMatchers("/actuator/health", "/actuator/info").permitAll()
                .pathMatchers("/api/public/**").permitAll()
                .pathMatchers("/api/auth/**").permitAll()

                // Endpoint khusus berdasarkan peran
                .pathMatchers("/api/admin/**").hasRole("ADMIN")
                .pathMatchers(HttpMethod.DELETE, "/api/**").hasRole("ADMIN")

                // Selebihnya memerlukan autentikasi
                .anyExchange().authenticated()
            )

            // OAuth2 Resource Server dengan JWT
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .jwtAuthenticationConverter(jwtAuthenticationConverter())
                )
            )

            .build();
    }

    @Bean
    public ReactiveJwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter =
            new JwtGrantedAuthoritiesConverter();
        // Mengambil peran dari claim "roles"
        grantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
        grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");

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

        return converter;
    }
}
TokenRelayFilter.javajava
// Penerusan token ke layanan downstream
@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 -> {
                // Meneruskan token ke layanan downstream
                ServerHttpRequest request = exchange.getRequest()
                    .mutate()
                    .header(HttpHeaders.AUTHORIZATION, "Bearer " + jwt.getTokenValue())
                    // Menambahkan informasi pengguna dari 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() {
        // Setelah autentikasi, sebelum routing
        return SecurityWebFiltersOrder.AUTHENTICATION.getOrder() + 1;
    }
}

Siap menguasai wawancara Spring Boot Anda?

Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.

Pertanyaan 12: Apa praktik terbaik pemantauan dan observability?

Pemantauan gateway sangat penting untuk mengidentifikasi masalah performa dan ketersediaan dalam arsitektur microservices.

yaml
# application.yml
# Konfigurasi 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 metrik kustom
@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();

        // Mengambil layanan tujuan dari rute
        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 untuk latensi
        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);

        // Penghitung permintaan
        meterRegistry.counter(
            "gateway.requests.total",
            "route", routeId,
            "status", statusCode
        ).increment();

        // Log permintaan lambat
        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
// Propagasi konteks tracing
@Component
@RequiredArgsConstructor
public class TracingFilter implements GlobalFilter, Ordered {

    private final Tracer tracer;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // Membuat atau mengambil span tracing
        Span span = tracer.nextSpan()
            .name("gateway-request")
            .tag("http.method", exchange.getRequest().getMethod().name())
            .tag("http.url", exchange.getRequest().getURI().toString())
            .start();

        // Menyuntikkan 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;
    }
}

Tabel metrik utama:

text
| Metrik                            | Deskripsi                        |
|-----------------------------------|----------------------------------|
| gateway.request.duration          | Latensi per rute/status          |
| gateway.requests.total            | Penghitung permintaan            |
| resilience4j.circuitbreaker.state | Status circuit breaker           |
| http.server.requests              | Metrik HTTP standar              |
| spring.cloud.gateway.routes       | Rute aktif                       |
Dashboard yang direkomendasikan

Gunakan Grafana dengan dashboard Spring Cloud Gateway dan Resilience4j untuk memvisualisasikan metrik. Konfigurasikan peringatan pada latensi P99 dan tingkat kesalahan.

Kesimpulan

Spring Cloud Gateway adalah komponen penting dalam arsitektur microservices modern. Poin-poin kunci untuk diingat saat wawancara:

Arsitektur dan konsep:

  • ✅ Routes, Predicates, dan Filters membentuk model dasar
  • ✅ Arsitektur reaktif dengan WebFlux dan Netty
  • ✅ Integrasi native dengan ekosistem Spring Cloud

Fitur penting:

  • ✅ Routing dinamis berdasarkan banyak kriteria
  • ✅ Filter pre/post untuk transformasi permintaan dan respons
  • ✅ Load balancing dengan Spring Cloud LoadBalancer

Ketahanan dan keamanan:

  • ✅ Circuit breaker dengan Resilience4j dan fallback
  • ✅ Retry dengan backoff eksponensial dan jitter
  • ✅ Autentikasi OAuth2/JWT terpusat

Observability:

  • ✅ Metrik Micrometer dengan tag per rute
  • ✅ Tracing terdistribusi dengan propagasi konteks
  • ✅ Health check dan endpoint actuator

Menguasai Spring Cloud Gateway menunjukkan pemahaman mendalam tentang pola microservices dan tantangan skalabilitas. Kompetensi ini sangat penting untuk merancang API Gateway yang andal dan berkinerja tinggi.

Mulai berlatih!

Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.

Tag

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

Bagikan

Artikel terkait