Contents
- Setup & First Client
- Request Annotations — Path, Query, Body, Headers
- Per-Client Configuration
- Request Interceptors — Auth & Tracing Headers
- Error Decoder — Custom Exception Mapping
- Circuit Breaker Fallbacks
- Service Discovery Integration
- Logging & Timeouts
<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.