Contents

Add the following to your pom.xml (Maven) or build.gradle (Gradle). Spring Boot manages compatible versions automatically when you use the Spring Boot BOM.

<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()); } }

The class below shows the implementation. Key points are highlighted in the inline comments.

@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); }

The class below shows the implementation. Key points are highlighted in the inline comments.

// 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

The class below shows the implementation. Key points are highlighted in the inline comments.

// 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.

The class below shows the implementation. Key points are highlighted in the inline comments.

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

The YAML below shows the complete configuration for this feature. Adjust the values to match your environment.

# 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 { /* ... */ }

The class below shows the implementation. Key points are highlighted in the inline comments.

// 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

The class below shows the implementation. Key points are highlighted in the inline comments.

// 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.