Spring Boot
OpenFeign
Spring Cloud OpenFeign is a declarative HTTP client for microservices. Instead of writing RestTemplate or WebClient boilerplate, you define an interface annotated with @FeignClient and Spring generates the implementation at runtime. Feign integrates automatically with Eureka for service discovery, Spring Cloud LoadBalancer for client-side load balancing, and Resilience4j for circuit breaking and fallbacks.
Setup and Dependencies
Add the OpenFeign starter to any microservice that needs to call another service. Enable it with @EnableFeignClients on the main application class or a @Configuration class. Feign integrates automatically with the Eureka client and Spring Cloud LoadBalancer starters when they are on the classpath.
XML
<!-- pom.xml: -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- OpenFeign declarative HTTP client: -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- Eureka client — Feign uses it for service-name resolution: -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- Circuit breaker — enables @FeignClient fallback: -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
</dependencies>
// ── Enable Feign on the main application class: ───────────────────────
@SpringBootApplication
@EnableFeignClients // scans for @FeignClient interfaces
@EnableDiscoveryClient
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
// ── Alternatively, scan a specific package: ───────────────────────────
@EnableFeignClients(basePackages = "com.example.orderservice.client")Declaring a Feign Client
A Feign client is a plain Java interface annotated with @FeignClient. The name attribute must match the spring.application.name of the target service registered in Eureka. Spring generates a proxy implementation that handles HTTP serialisation, load balancing, and error mapping — no implementation class is needed.
Java
// ── Basic Feign client: ───────────────────────────────────────────────
// 'name' = spring.application.name of the target service in Eureka
@FeignClient(name = "user-service")
public interface UserClient {
@GetMapping("/api/users/{id}")
UserResponse findById(@PathVariable("id") Long id);
@GetMapping("/api/users")
List<UserResponse> findAll(
@RequestParam(value = "role", required = false) String role
);
@PostMapping("/api/users")
UserResponse create(@RequestBody CreateUserRequest request);
@PutMapping("/api/users/{id}")
UserResponse update(
@PathVariable("id") Long id,
@RequestBody UpdateUserRequest request
);
@DeleteMapping("/api/users/{id}")
void delete(@PathVariable("id") Long id);
}
// ── Feign client with explicit URL (bypasses Eureka — useful for tests): ─
@FeignClient(name = "user-service-local",
url = "http://localhost:8081")
public interface UserClientLocal {
@GetMapping("/api/users/{id}")
UserResponse findById(@PathVariable("id") Long id);
}
// ── Inject and use exactly like a regular Spring bean: ────────────────
@Service
@RequiredArgsConstructor
public class OrderService {
private final UserClient userClient; // Feign proxy injected
private final InventoryClient inventoryClient;
public OrderResponse placeOrder(CreateOrderRequest request) {
// Feign handles: HTTP, serialisation, load balancing, retries.
UserResponse user = userClient.findById(request.getUserId());
InventoryResponse inv =
inventoryClient.findByProduct(request.getProductId());
Order order = Order.builder()
.userId(user.getId())
.productId(inv.getProductId())
.quantity(request.getQuantity())
.build();
return OrderResponse.from(orderRepository.save(order));
}
}Fallback and Circuit Breaker
When a downstream service is unavailable, Feign can return a fallback response instead of propagating an exception. The fallback class implements the Feign client interface and provides default return values for each method. Resilience4j powers the circuit breaker that triggers the fallback.
Java
// ── Enable circuit breaker support for Feign: ────────────────────────
// application.yml:
// spring:
// cloud:
// openfeign:
// circuitbreaker:
// enabled: true
// ── Feign client with fallback class: ─────────────────────────────────
@FeignClient(
name = "user-service",
fallback = UserClientFallback.class
)
public interface UserClient {
@GetMapping("/api/users/{id}")
UserResponse findById(@PathVariable("id") Long id);
@GetMapping("/api/users")
List<UserResponse> findAll(@RequestParam String role);
@PostMapping("/api/users")
UserResponse create(@RequestBody CreateUserRequest request);
}
// ── Fallback implementation: ──────────────────────────────────────────
@Component // must be a Spring bean
public class UserClientFallback implements UserClient {
@Override
public UserResponse findById(Long id) {
// Return a safe default — caller can detect the placeholder:
return UserResponse.builder()
.id(id)
.name("Unknown User")
.email("N/A")
.build();
}
@Override
public List<UserResponse> findAll(String role) {
return Collections.emptyList(); // empty list — safe for iteration
}
@Override
public UserResponse create(CreateUserRequest request) {
// Cannot create user if service is down — throw business exception:
throw new ServiceUnavailableException(
"User service is currently unavailable. " +
"Cannot create user at this time.");
}
}
// ── Fallback factory — access the cause of the failure: ───────────────
@Component
public class UserClientFallbackFactory
implements FallbackFactory<UserClient> {
@Override
public UserClient create(Throwable cause) {
return new UserClient() {
@Override
public UserResponse findById(Long id) {
log.error("UserService findById failed for id={}: {}",
id, cause.getMessage());
return UserResponse.builder().id(id).name("Unknown").build();
}
@Override
public List<UserResponse> findAll(String role) {
log.error("UserService findAll failed: {}", cause.getMessage());
return Collections.emptyList();
}
@Override
public UserResponse create(CreateUserRequest request) {
log.error("UserService create failed: {}", cause.getMessage());
throw new ServiceUnavailableException(cause.getMessage());
}
};
}
}
// ── Use FallbackFactory on the @FeignClient: ──────────────────────────
@FeignClient(
name = "user-service",
fallbackFactory = UserClientFallbackFactory.class // use factory instead
)
public interface UserClient { ... }Error Handling with ErrorDecoder
By default, Feign throws a FeignException for any non-2xx response. An ErrorDecoder intercepts those responses and maps HTTP status codes to typed business exceptions, enabling callers to handle specific failure cases such as 404 Not Found or 409 Conflict without catching generic FeignExceptions.
Java
// ── Custom ErrorDecoder: ─────────────────────────────────────────────
@Component
public class FeignErrorDecoder implements ErrorDecoder {
private final ErrorDecoder defaultDecoder = new Default();
@Override
public Exception decode(String methodKey, Response response) {
return switch (response.status()) {
case 400 -> new BadRequestException(
"Bad request sent to: " + methodKey);
case 401 -> new UnauthorizedException(
"Unauthorized — check service credentials");
case 403 -> new ForbiddenException(
"Access denied: " + methodKey);
case 404 -> {
// Extract the requested ID from the response body if present:
String body = extractBody(response);
yield new ResourceNotFoundException(
"Resource not found [" + methodKey + "]: " + body);
}
case 409 -> new ConflictException(
"Conflict in: " + methodKey);
case 503 -> new ServiceUnavailableException(
"Service unavailable: " + methodKey);
default -> defaultDecoder.decode(methodKey, response);
};
}
private String extractBody(Response response) {
try {
if (response.body() == null) return "";
return new String(
response.body().asInputStream().readAllBytes(),
StandardCharsets.UTF_8);
} catch (IOException e) {
return "";
}
}
}
// ── Register ErrorDecoder as a global bean: ───────────────────────────
// Declaring it as a @Component (above) makes it global — applies to
// ALL Feign clients in the application.
// ── Per-client ErrorDecoder (via @FeignClient configuration): ─────────
@Configuration
public class UserClientConfig {
@Bean
public ErrorDecoder userServiceErrorDecoder() {
return (methodKey, response) -> switch (response.status()) {
case 404 -> new UserNotFoundException(
"User not found — method: " + methodKey);
default -> new Default().decode(methodKey, response);
};
}
}
@FeignClient(
name = "user-service",
configuration = UserClientConfig.class // scoped to this client only
)
public interface UserClient { ... }Request Interceptor
A RequestInterceptor runs before every Feign request is sent. It is the correct place to propagate headers from the incoming request — such as the JWT Authorization header, correlation/trace IDs, or tenant identifiers — to all downstream calls made by this service.
Java
// ── Propagate JWT from incoming request to all Feign calls: ───────────
@Component
public class AuthHeaderPropagationInterceptor
implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
// Retrieve the current HTTP request from the Spring request context:
ServletRequestAttributes attributes =
(ServletRequestAttributes)
RequestContextHolder.getRequestAttributes();
if (attributes == null) return;
HttpServletRequest request = attributes.getRequest();
// Forward Authorization header if present:
String authHeader = request.getHeader(
HttpHeaders.AUTHORIZATION);
if (StringUtils.hasText(authHeader)) {
template.header(HttpHeaders.AUTHORIZATION, authHeader);
}
// Forward correlation ID for distributed tracing:
String correlationId = request.getHeader("X-Correlation-Id");
if (StringUtils.hasText(correlationId)) {
template.header("X-Correlation-Id", correlationId);
}
// Forward tenant identifier for multi-tenant systems:
String tenantId = request.getHeader("X-Tenant-Id");
if (StringUtils.hasText(tenantId)) {
template.header("X-Tenant-Id", tenantId);
}
}
}
// ── Per-client interceptor — add API key for a third-party service: ───
@Configuration
public class ExternalApiClientConfig {
@Value("${external.api.key}")
private String apiKey;
@Bean
public RequestInterceptor apiKeyInterceptor() {
return template -> template.header("X-Api-Key", apiKey);
}
}
@FeignClient(
name = "external-api",
url = "${external.api.url}",
configuration = ExternalApiClientConfig.class
)
public interface ExternalApiClient {
@GetMapping("/data")
ExternalDataResponse getData(@RequestParam String query);
}Timeout and Retry Configuration
Feign uses its own connection and read timeouts that are separate from the underlying HTTP client. Retries can be configured globally or per client. For production use, connect Feign to Resilience4j retry policies rather than Feign's built-in retry, which is limited to network-level IOExceptions and does not handle HTTP 5xx responses.
Java
// ── Timeout configuration (application.yml): ─────────────────────────
// spring:
// cloud:
// openfeign:
// client:
// config:
// default: # applies to all Feign clients
// connect-timeout: 2000 # ms — time to establish connection
// read-timeout: 5000 # ms — time to wait for response
//
// user-service: # overrides for user-service only
// connect-timeout: 1000
// read-timeout: 3000
// ── Feign client with per-client timeout config: ──────────────────────
@FeignClient(
name = "user-service",
configuration = UserFeignConfig.class
)
public interface UserClient { ... }
@Configuration
public class UserFeignConfig {
@Bean
public Request.Options requestOptions() {
return new Request.Options(
1000, TimeUnit.MILLISECONDS, // connectTimeout
3000, TimeUnit.MILLISECONDS, // readTimeout
true // followRedirects
);
}
}
// ── Resilience4j Retry via Feign (recommended over built-in retry): ───
// application.yml:
// spring:
// cloud:
// openfeign:
// circuitbreaker:
// enabled: true
//
// resilience4j:
// retry:
// instances:
// user-service:
// max-attempts: 3
// wait-duration: 500ms
// retry-exceptions:
// - feign.FeignException.ServiceUnavailable
// - java.net.ConnectException
// ignore-exceptions:
// - com.example.exception.UserNotFoundException # don't retry 404
// ── Logging Feign requests/responses for debugging: ───────────────────
// application.yml:
// spring:
// cloud:
// openfeign:
// client:
// config:
// default:
// logger-level: FULL # NONE | BASIC | HEADERS | FULL
//
// logging:
// level:
// com.example.client.UserClient: DEBUG # must also set logger to DEBUG
//
// Logger levels:
// NONE → no logging (default, use in production)
// BASIC → method, URL, status code, execution time
// HEADERS → BASIC + request/response headers
// FULL → HEADERS + request/response body and metadata