Why this matters
RestTemplateis deprecated — it blocks a thread for every outbound call and lacks a fluent API.RestClient(Boot 3.2+) is the modern blocking client: fluent, composable, and works with virtual threads.WebClientis the reactive non-blocking option; use it when the calling code is already on WebFlux or you need streaming responses.
HTTP request
DispatcherServlet
Controller
Service
JSON response
Three HTTP clients at a glance
RestTemplate— synchronous, blocking, deprecated since Spring 6. Do not start new code with it.RestClient— synchronous, blocking, fluent builder API. Default choice for MVC apps on Boot 3.2+.WebClient— reactive, non-blocking, returnsMono/Flux. Pairs with WebFlux or fire-and-forget async.@HttpExchange— declarative interface backed byRestClientorWebClientviaHttpServiceProxyFactory.- Observation — all three integrate with Micrometer tracing when observation is enabled.
RestClient — the modern blocking client
@Configuration
public class HttpClientConfig {
@Bean
public RestClient inventoryRestClient(RestClient.Builder builder) {
return builder
.baseUrl("http://inventory-service:8081")
.defaultHeader("X-Service-Name", "bookstore-api")
.build();
}
}
@Service
public class InventoryClient {
private final RestClient restClient;
public InventoryClient(RestClient inventoryRestClient) {
this.restClient = inventoryRestClient;
}
public StockLevel checkStock(String isbn) {
return restClient.get()
.uri("/api/stock/{isbn}", isbn)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, (req, res) -> {
throw new InventoryNotFoundException(isbn);
})
.body(StockLevel.class);
}
public void reserveStock(String isbn, int quantity) {
restClient.post()
.uri("/api/stock/{isbn}/reserve", isbn)
.body(new ReserveRequest(quantity))
.retrieve()
.toBodilessEntity();
}
}
WebClient — reactive outbound calls
@Bean
public WebClient paymentWebClient(WebClient.Builder builder) {
return builder
.baseUrl("http://payment-service:8082")
.filter(ExchangeFilterFunction.ofRequestProcessor(
req -> Mono.just(ClientRequest.from(req)
.header("X-Service-Name", "bookstore-api")
.build())))
.build();
}
@Service
public class PaymentClient {
private final WebClient webClient;
public Mono<PaymentResult> charge(ChargeRequest request) {
return webClient.post()
.uri("/api/charges")
.bodyValue(request)
.retrieve()
.bodyToMono(PaymentResult.class)
.timeout(Duration.ofSeconds(5))
.retryWhen(Retry.backoff(3, Duration.ofMillis(200)));
}
}
Declarative clients with @HttpExchange
@HttpExchange("/api/reviews")
public interface ReviewServiceClient {
@GetExchange("/{isbn}")
ReviewSummary getReviews(@PathVariable String isbn);
@PostExchange
void submitReview(@RequestBody SubmitReviewRequest request);
}
@Configuration
public class DeclarativeClientConfig {
@Bean
ReviewServiceClient reviewClient(RestClient.Builder builder) {
RestClient restClient = builder.baseUrl("http://review-service:8083").build();
RestClientAdapter adapter = RestClientAdapter.create(restClient);
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
return factory.createClient(ReviewServiceClient.class);
}
}
Quick recall
Everything you need if you only revisit this box.
RestClientis the default blocking HTTP client in Boot 3.2+ — fluent API, replacesRestTemplate.WebClientis for reactive/non-blocking stacks or when composing multiple async upstream calls.@HttpExchangeinterfaces +HttpServiceProxyFactoryprovide declarative clients over either backend.- Configure timeouts and retries explicitly — defaults will hang or fail silently under upstream outages.
- Enable observation on clients so outbound calls appear as child spans in distributed traces.
Test yourself
Answer these before moving on — recall is what makes it stick.