OpenFeign Client
Spring Cloud OpenFeign turns an HTTP API into a Java interface: you declare methods with Spring MVC annotations and Feign generates the calling code at runtime. It removes boilerplate compared to hand-written clients and is popular in microservices. This page covers setup, @FeignClient, configuration, and error decoding. For the general HTTP-client landscape see WebClient & RestClient.
Dependencies and enabling
Add the Spring Cloud OpenFeign starter and import the Spring Cloud BOM so versions align with your Spring Boot version.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2025.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Enable client scanning on a configuration class:
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableFeignClients
public class FeignConfig {
}
Note: OpenFeign is part of Spring Cloud, so its release train (e.g.
2025.0.x) must match your Spring Boot 3.5 line. Always use the BOM rather than pinning a raw version.
Declaring a @FeignClient
Define an interface, annotate it, and use the same MVC annotations you would on a controller. Feign implements it as a bean you can inject.
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
import java.util.List;
public record User(Long id, String name, String email) {}
public record CreateUser(String name, String email) {}
@FeignClient(name = "user-service", url = "${clients.user-service.url}")
public interface UserClient {
@GetMapping("/users/{id}")
User getUser(@PathVariable Long id);
@GetMapping("/users")
List<User> list(@RequestParam(defaultValue = "0") int page);
@PostMapping("/users")
User create(@RequestBody CreateUser body);
}
clients.user-service.url=https://api.example.com
Inject and call it like any bean — no HTTP plumbing in your service:
@Service
public class ProfileService {
private final UserClient userClient;
public ProfileService(UserClient userClient) {
this.userClient = userClient;
}
public User profile(Long id) {
return userClient.getUser(id);
}
}
Request issued by Feign:
GET https://api.example.com/users/42
Accept: application/json
Output (deserialized):
{ "id": 42, "name": "Ada Lovelace", "email": "[email protected]" }
Tip: Omit
urland use onlynamewhen combined with service discovery (e.g. Eureka) — Feign then resolves the host through the load balancer. See Service Discovery.
Configuration
Tune timeouts and logging globally in application.yml:
spring:
cloud:
openfeign:
client:
config:
default:
connectTimeout: 2000
readTimeout: 5000
loggerLevel: basic
user-service:
readTimeout: 8000
Feign’s logger levels are none, basic, headers, and full. Logging only emits when the surrounding logger is at DEBUG:
logging.level.com.example.client.UserClient=DEBUG
You can also supply a per-client Java configuration class for custom beans (interceptors, decoders, contracts):
@FeignClient(name = "user-service", url = "${clients.user-service.url}",
configuration = UserClientConfig.class)
public interface UserClient { ... }
public class UserClientConfig {
@Bean
public RequestInterceptor authInterceptor() {
return template -> template.header("Authorization", "Bearer " + TokenHolder.get());
}
}
Warning: A
configurationclass referenced from@FeignClientshould not be@Configurationannotated, or its beans leak into the global context and apply to every client.
Error decoding
By default Feign throws FeignException on non-2xx responses. Supply an ErrorDecoder to map upstream statuses to your own exceptions.
import feign.Response;
import feign.codec.ErrorDecoder;
public class UserErrorDecoder implements ErrorDecoder {
private final ErrorDecoder defaultDecoder = new Default();
@Override
public Exception decode(String methodKey, Response response) {
return switch (response.status()) {
case 404 -> new NotFoundException("User not found");
case 400 -> new BadRequestException("Invalid request to user-service");
default -> defaultDecoder.decode(methodKey, response);
};
}
}
Register it in the client’s configuration:
public class UserClientConfig {
@Bean
public ErrorDecoder errorDecoder() {
return new UserErrorDecoder();
}
}
OpenFeign vs RestClient/WebClient
| Aspect | OpenFeign | RestClient / WebClient |
|---|---|---|
| Style | Declarative interface | Imperative fluent calls |
| Boilerplate | Minimal | More per call |
| Dependency | Spring Cloud | Core Spring |
| Discovery / LB integration | Built in | Manual / via load balancer |
| Best for | Many service-to-service calls | One-off or fine-grained control |
Pitfalls
- Forgetting
@EnableFeignClientsmeans the interfaces are never proxied — injection fails at startup. - Mismatched Spring Cloud and Spring Boot versions cause obscure runtime errors; rely on the BOM.
- Feign uses Spring MVC annotations but it is a client, so
@PathVariable/@RequestParamdescribe the outgoing request, not an incoming one.