How to use Spring WebClient as a modern, non-blocking replacement for RestTemplate — including when it's fine to just call .block().
RestTemplate has been in maintenance mode since Spring 5. The replacement is WebClient — part of Spring WebFlux, but usable in any Spring Boot application whether or not you’ve committed to a fully reactive stack. The problem is that WebClient lives in a reactive library, which puts some developers off: they see Mono, Flux, and retryWhen and assume they need to understand the full reactive programming model before they can use it.
You don’t. WebClient is the right HTTP client for Spring Boot applications today. This post covers how to use it pragmatically, including the places where it’s entirely reasonable to block and treat it like a synchronous call.
WebClient lives in spring-boot-starter-webflux. Add it alongside your existing spring-boot-starter-web — the two coexist without conflict:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
Don’t use WebClient.create() directly in service code. Configure a bean with your base URL, timeouts, and default headers:
@Configuration
public class HttpClientConfig {
@Bean
public WebClient betfairWebClient(
@Value("${betfair.exchange.base-url}") String baseUrl,
@Value("${betfair.exchange.timeout-ms:10000}") int timeoutMs) {
var httpClient = HttpClient.create()
.responseTimeout(Duration.ofMillis(timeoutMs))
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);
return WebClient.builder()
.baseUrl(baseUrl)
.clientConnector(new ReactorClientHttpConnector(httpClient))
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader("X-Application", "MyTradingApp/1.0")
.build();
}
}
Inject WebClient as a constructor dependency. One bean per external service.
A basic GET with response body mapping:
public MarketBook getMarketBook(String marketId) {
return betfairWebClient.get()
.uri("/listMarketBook?marketIds={id}", marketId)
.retrieve()
.bodyToMono(MarketBook.class)
.block();
}
The .block() call converts the reactive Mono<MarketBook> into a synchronous result. If you’re in a traditional Spring MVC application with blocking servlet threads, this is fine — you’re not on an event loop, and blocking a platform thread has no consequence.
A POST with request body:
public PlaceExecutionReport placeOrders(PlaceOrdersRequest request) {
return betfairWebClient.post()
.uri("/placeOrders")
.bodyValue(request)
.retrieve()
.bodyToMono(PlaceExecutionReport.class)
.block();
}
Use onStatus() to map HTTP error codes to domain exceptions before .block() swallows them:
public MarketCatalogue getMarketCatalogue(String marketId) {
return betfairWebClient.get()
.uri("/listMarketCatalogue?filter={id}", marketId)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, response ->
response.bodyToMono(String.class)
.map(body -> new BetfairClientException(
"Client error %d: %s".formatted(response.statusCode().value(), body)))
)
.onStatus(HttpStatusCode::is5xxServerError, response ->
Mono.error(new BetfairServiceException(
"Betfair API returned " + response.statusCode()))
)
.bodyToMono(MarketCatalogue.class)
.block();
}
Without this, a 4xx or 5xx response throws a generic WebClientResponseException. Mapping it at the HTTP layer keeps your service code clean.
Transient failures in external APIs are a fact of life. retryWhen gives you backoff with jitter:
public PriceProjection getPrices(String marketId) {
return betfairWebClient.get()
.uri("/listRunnerBook?marketId={id}", marketId)
.retrieve()
.bodyToMono(PriceProjection.class)
.retryWhen(Retry.backoff(3, Duration.ofMillis(200))
.maxBackoff(Duration.ofSeconds(2))
.jitter(0.5)
.filter(ex -> ex instanceof BetfairServiceException))
.block();
}
The .filter() predicate restricts retries to server errors — don’t retry client errors like 400 Bad Request or 401 Unauthorized, as retrying won’t help.
If you’re on a reactive stack (Spring WebFlux controllers returning Mono/Flux), never call .block() — it will deadlock the event loop. Return the Mono directly from your service and let the framework handle subscription:
// In a reactive controller:
public Mono<ResponseEntity<MarketBook>> getMarket(String marketId) {
return betfairWebClient.get()
.uri("/listMarketBook?marketIds={id}", marketId)
.retrieve()
.bodyToMono(MarketBook.class)
.map(ResponseEntity::ok);
}
In a traditional Spring MVC application, .block() is the pragmatic choice. The non-blocking benefit only materialises when you’re actually on an event-loop thread.
Spring 6.1 introduced RestClient — a synchronous HTTP client with a fluent API similar to WebClient, but without the reactive types. If you have no interest in reactive and just want a modern replacement for RestTemplate, RestClient is simpler. If you need retries, timeouts via Reactor’s scheduler, or want the option to go non-blocking later, WebClient is the stronger choice.
On a recent trading platform project, WebClient with retry backoff reduced timeout-related order placement failures during Betfair API instability windows from a persistent problem to an occasional log entry. The configuration overhead is front-loaded; the resilience payoff is ongoing.
If you’re modernising legacy RestTemplate usage or building new integrations against external APIs, get in touch.