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