Contents

<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> // Enable Feign on the main application class @SpringBootApplication @EnableFeignClients(basePackages = "com.example.clients") public class OrderServiceApplication { } // Define the client interface — Spring generates the implementation @FeignClient( name = "payment-service", // logical service name (used for discovery) url = "${payment.service.url}", // explicit URL (override discovery) path = "/v1" // common path prefix for all methods ) public interface PaymentClient { @PostMapping("/charges") ChargeResponse charge(@RequestBody ChargeRequest request); @GetMapping("/charges/{chargeId}") ChargeResponse getCharge(@PathVariable("chargeId") String chargeId); @DeleteMapping("/charges/{chargeId}") void refund(@PathVariable("chargeId") String chargeId); } // Inject and use like any Spring bean — no boilerplate @Service @RequiredArgsConstructor public class CheckoutService { private final PaymentClient paymentClient; public OrderConfirmation checkout(Cart cart) { ChargeResponse charge = paymentClient.charge( new ChargeRequest(cart.total(), cart.paymentToken())); return OrderConfirmation.of(charge.id()); } } @FeignClient(name = "inventory-service") public interface InventoryClient { // Path variables @GetMapping("/products/{id}") Product getProduct(@PathVariable("id") Long id); // Query parameters @GetMapping("/products") Page<Product> searchProducts( @RequestParam("category") String category, @RequestParam("page") int page, @RequestParam("size") int size); // Request body (POST / PUT) @PostMapping("/products") Product createProduct(@RequestBody CreateProductRequest request); @PutMapping("/products/{id}") Product updateProduct(@PathVariable("id") Long id, @RequestBody UpdateProductRequest request); // Static header on method @GetMapping("/internal/stats") @Headers("X-Internal-Request: true") InventoryStats getStats(); // Dynamic header per call @GetMapping("/products/{id}/stock") StockLevel getStock(@PathVariable("id") Long id, @RequestHeader("X-Region") String region); // URI template map @GetMapping("/products/search") List<Product> search(@SpringQueryMap ProductSearchRequest params); } // Custom configuration class — NOT annotated with @Configuration // (to avoid applying globally to all clients) public class PaymentClientConfig { @Bean public Contract feignContract() { return new SpringMvcContract(); // default — use Spring MVC annotations } @Bean public Retryer retryer() { // Retry up to 3 times with 100ms initial backoff, max 1s return new Retryer.Default(100, TimeUnit.SECONDS.toMillis(1), 3); } @Bean public Logger.Level feignLoggerLevel() { return Logger.Level.FULL; // NONE, BASIC, HEADERS, FULL } @Bean public Request.Options options() { return new Request.Options( 3, TimeUnit.SECONDS, // connect timeout 10, TimeUnit.SECONDS, // read timeout true); // follow redirects } } // Reference from the client @FeignClient( name = "payment-service", configuration = PaymentClientConfig.class // applies only to this client ) public interface PaymentClient { /* ... */ } # Or configure via properties (Spring Boot 2.2+) spring: cloud: openfeign: client: config: payment-service: # matches @FeignClient name connect-timeout: 3000 read-timeout: 10000 logger-level: full default: # applies to all clients connect-timeout: 5000 read-timeout: 10000 // RequestInterceptor — runs before every Feign request @Component public class AuthTokenInterceptor implements RequestInterceptor { private final TokenProvider tokenProvider; @Override public void apply(RequestTemplate template) { // Propagate the current user's JWT to downstream services String token = tokenProvider.getCurrentToken(); if (token != null) { template.header(HttpHeaders.AUTHORIZATION, "Bearer " + token); } } } // Propagate tracing headers (if not using auto-instrumented Micrometer Tracing) @Component public class TracingInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate template) { String traceId = MDC.get("traceId"); String spanId = MDC.get("spanId"); if (traceId != null) template.header("X-B3-TraceId", traceId); if (spanId != null) template.header("X-B3-SpanId", spanId); } } When using Micrometer Tracing or Spring Cloud Sleuth, trace context is propagated automatically — you don't need a manual interceptor. The interceptor pattern is useful for custom auth tokens or tenant headers that tracing libraries don't cover. // Map HTTP error responses to meaningful domain exceptions @Component public class PaymentErrorDecoder implements ErrorDecoder { private final ObjectMapper objectMapper; private final ErrorDecoder defaultDecoder = new Default(); @Override public Exception decode(String methodKey, Response response) { return switch (response.status()) { case 400 -> { try { ErrorResponse body = objectMapper.readValue( response.body().asInputStream(), ErrorResponse.class); yield new PaymentValidationException(body.message()); } catch (IOException e) { yield new PaymentException("Invalid request to payment service"); } } case 402 -> new InsufficientFundsException("Card declined"); case 404 -> new ChargeNotFoundException("Charge not found: " + methodKey); case 429 -> new RateLimitException("Payment service rate limit exceeded"); case 503 -> new ServiceUnavailableException("Payment service is down"); default -> defaultDecoder.decode(methodKey, response); }; } } # Enable circuit breaker support for Feign spring: cloud: openfeign: circuitbreaker: enabled: true // Option 1: Fallback class @Component public class InventoryClientFallback implements InventoryClient { @Override public Product getProduct(Long id) { // Return a cached/default value when inventory service is down return Product.unavailable(id); } @Override public Page<Product> searchProducts(String category, int page, int size) { return Page.empty(); } } // Reference in the client @FeignClient(name = "inventory-service", fallback = InventoryClientFallback.class) public interface InventoryClient { /* ... */ } // Option 2: FallbackFactory — access the cause exception @Component public class InventoryClientFallbackFactory implements FallbackFactory<InventoryClient> { @Override public InventoryClient create(Throwable cause) { log.error("Inventory service call failed: {}", cause.getMessage()); return new InventoryClientFallback(); } } @FeignClient(name = "inventory-service", fallbackFactory = InventoryClientFallbackFactory.class) public interface InventoryClient { /* ... */ } // With Eureka on the classpath, @FeignClient name resolves via service registry // No URL needed — Feign + LoadBalancer pick a healthy instance automatically @FeignClient(name = "order-service") // matches spring.application.name in order-service public interface OrderClient { @GetMapping("/orders/{id}") Order getOrder(@PathVariable("id") Long id); } # application.yml — load balancer strategy spring: cloud: loadbalancer: ribbon: enabled: false # use Spring Cloud LoadBalancer, not legacy Ribbon openfeign: client: config: order-service: read-timeout: 5000 # Retry on different instance on failure spring: cloud: loadbalancer: retry: enabled: true max-retries-on-same-service-instance: 0 max-retries-on-next-service-instance: 2 retryable-status-codes: 500,502,503 // Enable Feign logging for a specific client // Logger level must be DEBUG in application logger logging: level: com.example.clients.PaymentClient: DEBUG // Logger.Level values: // NONE — no logging (default, best for production) // BASIC — method, URL, response code, execution time // HEADERS — BASIC + request/response headers // FULL — HEADERS + request/response body // Sample FULL output: // [PaymentClient#charge] ---> POST http://payment-service/v1/charges // [PaymentClient#charge] Content-Type: application/json // [PaymentClient#charge] {"amount":9999,"token":"tok_visa"} // [PaymentClient#charge] ---> END HTTP (45-byte body) // [PaymentClient#charge] <--- HTTP/1.1 200 OK (234ms) // [PaymentClient#charge] {"id":"ch_123","status":"succeeded"} // [PaymentClient#charge] <--- END HTTP (42-byte body) Never use Logger.Level.FULL in production — it logs full request and response bodies, which may contain sensitive data (PII, tokens, card numbers). Use BASIC in production and FULL only for local debugging.