RestClient & HTTP Service Clients
Calling other services with RestClient, declarative @HttpExchange interfaces registered with @ImportHttpServices, timeouts, error handling and retries.
Which client?
| Client | Style | Use in Spring Boot 4 |
|---|---|---|
RestClient |
Fluent, synchronous | ✓ Default choice for servlet apps (pairs perfectly with virtual threads) |
HTTP service clients (@HttpExchange interfaces) |
Declarative | ✓ Best for calling well-defined APIs |
WebClient |
Fluent, reactive | WebFlux apps, or streaming |
RestTemplate |
Template, synchronous | Legacy — in maintenance mode; prefer RestClient |
| OpenFeign | Declarative (Spring Cloud) | Superseded by HTTP service clients for most uses |
RestClient
Inject the auto-configured RestClient.Builder (it applies Boot’s message converters, observability and customizers) and build one client per downstream service:
@Configuration(proxyBeanMethods = false)
class InventoryClientConfig {
@Bean
RestClient inventoryRestClient(RestClient.Builder builder, InventoryProperties props) {
return builder
.baseUrl(props.baseUrl())
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.requestInterceptor(new BearerTokenInterceptor()) // auth, correlation ids, …
.build();
}
}
@Component
class InventoryClient {
private final RestClient http;
InventoryClient(RestClient inventoryRestClient) { this.http = inventoryRestClient; }
StockLevel stock(String sku) {
return http.get()
.uri("/stock/{sku}", sku)
.retrieve()
.onStatus(status -> status.value() == 404, (req, res) -> { throw new UnknownSkuException(sku); })
.body(StockLevel.class);
}
List<StockLevel> stock(List<String> skus) {
return http.get()
.uri(b -> b.path("/stock").queryParam("sku", skus).build())
.retrieve()
.body(new ParameterizedTypeReference<>() {}); // generic types
}
Reservation reserve(ReserveRequest request) {
ResponseEntity<Reservation> response = http.post()
.uri("/reservations")
.contentType(MediaType.APPLICATION_JSON)
.body(request)
.retrieve()
.toEntity(Reservation.class); // status + headers + body
return response.getBody();
}
}
By default, retrieve() throws HttpClientErrorException (4xx) or HttpServerErrorException (5xx). Use onStatus to translate them into meaningful domain exceptions, or exchange(...) for full control.
HTTP service clients — declarative interfaces
Describe the remote API as a Java interface. Spring generates the implementation (a proxy) backed by RestClient:
@HttpExchange("/stock")
public interface InventoryApi {
@GetExchange("/{sku}")
StockLevel stock(@PathVariable String sku);
@GetExchange
List<StockLevel> stock(@RequestParam("sku") List<String> skus);
@PostExchange("/reservations")
Reservation reserve(@RequestBody ReserveRequest request);
@DeleteExchange("/reservations/{id}")
void release(@PathVariable String id);
}
Spring Framework 7 removes the old boilerplate of wiring a HttpServiceProxyFactory by hand. Register whole groups of interfaces and configure them through properties:
@Configuration(proxyBeanMethods = false)
@ImportHttpServices(group = "inventory", types = InventoryApi.class)
@ImportHttpServices(group = "payments", basePackages = "com.shop.payments.api")
class HttpClientsConfig {}
spring:
http:
serviceclient:
inventory:
base-url: https://inventory.internal
read-timeout: 2s
payments:
base-url: https://payments.internal
connect-timeout: 1s
read-timeout: 5s
Then just inject the interface:
@Service
class CheckoutService {
private final InventoryApi inventory;
CheckoutService(InventoryApi inventory) { this.inventory = inventory; }
}
Interfaces are trivial to mock in tests and read like documentation of the remote API. API versioning works here too: @GetExchange(url = "/{sku}", version = "2.0").
Timeouts: always set them
The default JDK/Apache clients can wait forever for a slow downstream service — one hanging dependency then exhausts your threads. Configure both:
- Connect timeout — how long to establish a TCP connection (≈1–2 s)
- Read timeout — how long to wait for response data (based on the downstream’s SLA)
# global defaults for RestClient / RestTemplate built from Boot's builders
spring.http.clients.connect-timeout=2s
spring.http.clients.read-timeout=5s
Retries and resilience
Spring Framework 7 ships retry support in core — no extra library needed for simple cases:
@Configuration
@EnableResilientMethods
class ResilienceConfig {}
@Component
class PricingClient {
@Retryable(includes = HttpServerErrorException.class, maxAttempts = 3, delay = 200, multiplier = 2)
public Price price(String sku) { return http.get().uri("/prices/{sku}", sku).retrieve().body(Price.class); }
}
Only retry idempotent operations (GET, PUT, DELETE — or POSTs protected by an idempotency key). For circuit breakers and bulkheads, see Microservice Resilience.
Mark it complete to track your progress through Spring Boot 4.