Here is the translated article body:
Have you ever grown tired of the boilerplate that comes with RestTemplate when writing code to call external APIs? Creating HttpHeaders, wrapping it in HttpEntity, calling exchange()… over and over again. This becomes especially noticeable in a microservice architecture where you’re calling multiple services.
That’s where OpenFeign comes in. Simply define an interface and your HTTP client is ready to go — the calling code becomes dramatically cleaner. This article walks you through everything from adding the dependency to actual implementation, error handling, and the configuration you need for production.
What is OpenFeign?
OpenFeign (spring-cloud-openfeign) is a Spring Cloud integration library based on Netflix Feign. You annotate a Java interface with @FeignClient and define methods using Spring MVC annotations like @GetMapping — the HTTP client is then generated automatically.
The key distinction is: RestTemplate is imperative (you write code describing how to call), WebClient is reactive-capable, and OpenFeign is declarative (you describe what to call via an interface). If your focus is synchronous REST calls, OpenFeign keeps your code the simplest. For a detailed comparison with RestTemplate and WebClient, see this article.
Adding the Dependency
This article assumes Spring Boot 3.2.x or later.
For Maven, the standard approach is to manage versions via the Spring Cloud BOM.
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2023.0.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
For Gradle, the io.spring.dependency-management plugin is required. The dependencyManagement block won’t work without it, so make sure to add it.
plugins {
id 'org.springframework.boot' version '3.2.5'
id 'io.spring.dependency-management' version '1.1.4'
id 'java'
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:2023.0.3"
}
}
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
}
Next, add @EnableFeignClients to your main class. Without this, @FeignClient interfaces will not be registered as Beans.
@SpringBootApplication
@EnableFeignClients
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
To restrict the scan scope, you can specify @EnableFeignClients(basePackages = "com.example.client").
Basic @FeignClient Interface Definition
The minimal configuration looks like this:
@FeignClient(name = "user-service", url = "${external.api.url}")
public interface UserClient {
@GetMapping("/users/{id}")
UserResponse getUser(@PathVariable("id") Long id);
}
name is used as the Bean name. Externalizing url in application.yml makes it easy to switch per environment. No @Component annotation is needed — it is automatically registered as a Spring Bean and can be used with constructor injection.
Implementing GET Requests
Let’s look at typical patterns for path parameters and query parameters.
@FeignClient(name = "product-service", url = "${product.api.url}")
public interface ProductClient {
@GetMapping("/products/{id}")
ProductResponse getProduct(@PathVariable("id") Long id);
@GetMapping("/products")
List<ProductResponse> searchProducts(
@RequestParam("category") String category,
@RequestParam("page") int page);
@GetMapping("/products/{id}")
ResponseEntity<ProductResponse> getProductWithStatus(@PathVariable("id") Long id);
}
The response body is automatically deserialized by Jackson. If you also need the status code, return ResponseEntity<T>.
Implementing POST Requests
A POST that sends a JSON body is written like this:
@FeignClient(name = "order-service", url = "${order.api.url}")
public interface OrderClient {
@PostMapping("/orders")
OrderResponse createOrder(@RequestBody CreateOrderRequest request);
@PostMapping("/orders")
OrderResponse createOrderWithHeader(
@RequestHeader("X-Request-Id") String requestId,
@RequestBody CreateOrderRequest request);
}
Content-Type: application/json is set automatically. Use @RequestHeader to add arbitrary HTTP headers.
Error Handling with FeignException
By default, OpenFeign throws a FeignException when it receives a 4xx or 5xx response. A basic try-catch looks like this:
@Service
public class OrderService {
private final OrderClient orderClient;
public OrderResponse createOrder(CreateOrderRequest request) {
try {
return orderClient.createOrder(request);
} catch (FeignException.BadRequest e) {
throw new IllegalArgumentException("Invalid request");
} catch (FeignException e) {
if (e.status() >= 500) {
throw new ServiceUnavailableException("External service error: " + e.status()); // custom exception class
}
throw e;
}
}
}
The status code can be retrieved via e.status() (FeignException#status()). For integration with @ExceptionHandler, see the exception handling article.
Centralizing Error Handling with a Custom ErrorDecoder
When you want to unify error handling across multiple FeignClients, a custom ErrorDecoder is the way to go.
public class FeignErrorDecoder implements ErrorDecoder {
private final ErrorDecoder defaultDecoder = new Default();
@Override
public Exception decode(String methodKey, Response response) {
return switch (response.status()) {
case 404 -> new ResourceNotFoundException("Resource not found"); // custom exception class
case 403 -> new ForbiddenException("Access denied"); // custom exception class
case 503 -> new ServiceUnavailableException("Service unavailable"); // custom exception class
default -> defaultDecoder.decode(methodKey, response);
};
}
}
This class is registered as a @Bean in the Config class described below.
Timeout and Logging Configuration
Timeouts and log levels can be configured together in application.yml.
spring:
cloud:
openfeign:
client:
config:
default:
connect-timeout: 3000
read-timeout: 10000
order-service:
connect-timeout: 2000
read-timeout: 5000
logging:
level:
com.example.client: DEBUG
Use default to set global defaults, and override per client using the value of the name attribute. When a timeout is exceeded, a RetryableException is thrown — but with the default Retryer.NEVER_RETRY, no retry is performed and the exception propagates to the caller as-is.
Log level and ErrorDecoder are registered as @Bean in a Java Config class.
@Configuration
public class FeignConfig {
@Bean
public Logger.Level feignLoggerLevel() {
// BASIC for production, FULL during development
return Logger.Level.BASIC;
}
@Bean
public ErrorDecoder errorDecoder() {
return new FeignErrorDecoder();
}
}
If this FeignConfig class is within the component scan scope, it applies globally to all FeignClients. To apply it to a specific client only, use @FeignClient(configuration = FeignConfig.class) and omit the @Configuration annotation from the class itself.
Log levels have four tiers: NONE (default), BASIC (request/response summary), HEADERS (includes headers), and FULL (full information including body). Output requires logging.level to be set to DEBUG or higher.
Summary
With OpenFeign, the imperative boilerplate of RestTemplate is replaced by interface definitions, resulting in significantly cleaner code. The benefit is especially pronounced in microservice architectures where you need to call multiple services.
If you need WebFlux or reactive support, choose WebClient instead. When circuit breaking or retry logic becomes necessary, consider Resilience4j integration. For how to test OpenFeign, check out the article on testing external APIs with WireMock.