When Should I Use RestClient vs WebClient in Spring Boot?
I spent months using WebClient.block() for synchronous HTTP calls. My reasoning? Spring 5 documentation said RestTemplate would be deprecated, and WebClient was the future. So I forced WebClient into everything.
Then Spring 6.1 introduced RestClient. Turns out, I was doing it wrong the whole time.
The Confusion: How We Got Here
The Spring team created this mess unintentionally. In Spring 5.0 (2017), the documentation stated that RestTemplate would be deprecated in a future version. The implication was clear: WebClient is the replacement.
Developers like me took that at face value. We started using WebClient everywhere, even for simple synchronous calls. The .block() pattern became common:
WebClient webClient = WebClient.create("https://api.example.com");
User user = webClient.get() .uri("/users/{id}", userId) .retrieve() .bodyToMono(User.class) .block(); // This defeats the purpose of WebClientThis is wrong. WebClient is built for reactive, non-blocking operations. Calling .block() throws away its core benefit.
The Spring team quietly updated the documentation to say RestTemplate is in “maintenance mode” rather than deprecated. But the damage was done. Thousands of codebases had WebClient with .block() scattered throughout.
Spring 6.1’s RestClient is Spring’s admission: the future is not entirely reactive. Sometimes you just need a synchronous HTTP client.
RestClient: The Modern Synchronous Choice
RestClient is what RestTemplate should have been. It has a modern, fluent API designed specifically for synchronous HTTP calls.
When to Use RestClient
- Traditional servlet-based Spring MVC applications
- Synchronous, blocking HTTP requests
- Simple request-response patterns
- Migrating from RestTemplate
- Your team lacks reactive programming expertise
- Java 21+ with virtual threads enabled
RestClient Basic Usage
RestClient restClient = RestClient.create();
User user = restClient.get() .uri("https://api.example.com/users/{id}", userId) .retrieve() .body(User.class);RestClient with Configuration
For real applications, you’ll want shared configuration:
RestClient restClient = RestClient.builder() .baseUrl("https://api.example.com") .defaultHeader("Authorization", "Bearer " + accessToken) .defaultHeader("Accept", "application/json") .requestInterceptor(loggingInterceptor) .build();
// Now all calls use this configurationUser user = restClient.get() .uri("/users/{id}", userId) .retrieve() .body(User.class);Error Handling
import org.springframework.web.client.RestClientResponseException;
try { User user = restClient.get() .uri("/users/{id}", userId) .retrieve() .body(User.class);} catch (RestClientResponseException e) { if (e.getStatusCode().value() == 404) { throw new UserNotFoundException("User not found: " + userId); } throw new ApiException("API call failed: " + e.getMessage());}Why RestClient is Better Than RestTemplate
- Fluent API instead of template method pattern
- Better integration with modern Spring features
- Supports multiple HTTP clients: JDK HttpClient, Apache HTTP Components, Jetty, Reactor Netty
- Cleaner error handling
- Works naturally with virtual threads (Java 21+)
WebClient: The Reactive Powerhouse
WebClient remains the right choice when you actually need reactive features.
When to Use WebClient
- Reactive Spring WebFlux applications
- Asynchronous, non-blocking requirements
- Streaming large responses
- High concurrency with limited threads
- Reactive streams backpressure
- You’re already invested in the reactive ecosystem
WebClient for Reactive Calls
WebClient webClient = WebClient.create("https://api.example.com");
Mono<User> userMono = webClient.get() .uri("/users/{id}", userId) .retrieve() .bodyToMono(User.class);
// Chain reactive operationsuserMono .flatMap(user -> updateCache(user)) .subscribe(updatedUser -> log.info("Updated: {}", updatedUser));WebClient for Streaming
This is where WebClient shines - streaming multiple items:
Flux<User> usersFlux = webClient.get() .uri("/users/stream") .retrieve() .bodyToFlux(User.class);
usersFlux .filter(user -> user.isActive()) .take(100) .subscribe(user -> processUser(user));The Key Difference
With RestClient, the call completes and returns the result. With WebClient, you get a Mono or Flux that represents the future result. You chain operations on it, and nothing happens until you subscribe.
If you don’t understand or need that distinction, use RestClient.
The Virtual Threads Factor
Java 21 introduced virtual threads. This changes the calculus significantly.
A Reddit comment captured it well:
“When virtual threads were announced, reactive was doomed.”
Virtual threads make blocking code efficient. Instead of managing complex reactive chains, you can write simple blocking code that performs just as well under high concurrency.
// In application.propertiesspring.threads.virtual.enabled=true
// Your RestClient code stays simple and blocking@RestControllerpublic class UserController {
private final RestClient restClient;
public UserController(RestClient.Builder builder) { this.restClient = builder .baseUrl("https://api.example.com") .build(); }
@GetMapping("/users/{id}") public User getUser(@PathVariable String id) { // This runs on a virtual thread - efficient and simple return restClient.get() .uri("/users/{id}", id) .retrieve() .body(User.class); }}With virtual threads, each request gets its own lightweight thread. Blocking operations don’t tie up platform threads. You get the simplicity of synchronous code with the concurrency benefits that used to require reactive programming.
Decision Checklist
Use this checklist to decide:
USE RESTCLIENT WHEN:[x] Building traditional Spring MVC applications[x] Need synchronous, blocking HTTP calls[x] Migrating from RestTemplate[x] Using Java 21+ with virtual threads[x] Team lacks reactive programming expertise[x] Simple request-response patterns[x] Want easier debugging and stack traces
USE WEBCLIENT WHEN:[x] Building reactive Spring WebFlux applications[x] Need asynchronous, non-blocking calls[x] Streaming large datasets[x] Already invested in reactive ecosystem[x] Need reactive streams backpressure[x] High concurrency on limited hardware without virtual threads
AVOID:[ ] Using WebClient.block() - use RestClient instead[ ] Mixing reactive and blocking code inconsistentlyMigration Examples
From RestTemplate to RestClient
// Old: RestTemplateRestTemplate restTemplate = new RestTemplate();User user = restTemplate.getForObject( "https://api.example.com/users/{id}", User.class, userId);
// New: RestClientRestClient restClient = RestClient.create();User user = restClient.get() .uri("https://api.example.com/users/{id}", userId) .retrieve() .body(User.class);From WebClient.block() to RestClient
If your code has WebClient with .block(), migrate to RestClient:
// Anti-patternWebClient webClient = WebClient.create();User user = webClient.get() .uri("/users/{id}", userId) .retrieve() .bodyToMono(User.class) .block(); // Stop doing this
// Correct approachRestClient restClient = RestClient.create();User user = restClient.get() .uri("/users/{id}", userId) .retrieve() .body(User.class);Common Mistakes
Mistake 1: Using WebClient in Non-Reactive Applications
I did this. It adds complexity without benefit. Your Spring MVC app with WebClient and .block() everywhere is harder to debug and slower than using RestClient directly.
Mistake 2: Believing RestTemplate is Deprecated
It’s in maintenance mode. It still works. But for new code, RestClient is the better choice.
Mistake 3: Mixing Reactive and Blocking Code
Don’t use WebClient in a Spring MVC controller and call .block(). Don’t use RestClient in a WebFlux application. Pick one paradigm and stick with it.
Mistake 4: Ignoring Virtual Threads
If you’re on Java 21+, enable virtual threads. It makes RestClient the clear winner for most use cases.
Best Practices
For RestClient
- Use the builder pattern for shared configuration
- Set base URLs for microservice communication
- Configure timeouts via the underlying HTTP client
- Use interceptors for logging, authentication, and cross-cutting concerns
- Consider connection pooling for high-volume scenarios
@Configurationpublic class RestClientConfig {
@Bean public RestClient restClient(RestClient.Builder builder) { HttpClient httpClient = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .build();
return builder .baseUrl("https://api.example.com") .requestFactory(new JdkClientHttpRequestFactory(httpClient)) .defaultHeader("Accept", "application/json") .build(); }}For WebClient
- Stay reactive - never use
.block() - Handle errors with
onErrorResume - Configure connection pooling
- Use
Monofor single items,Fluxfor streams - Test with
StepVerifier
webClient.get() .uri("/users/{id}", userId) .retrieve() .bodyToMono(User.class) .onErrorResume(WebClientResponseException.NotFound.class, e -> Mono.just(new User("not-found"))) .onErrorResume(Exception.class, e -> Mono.error(new ApiException("Failed to fetch user", e)));The Bottom Line
RestClient is the modern choice for synchronous HTTP calls. WebClient remains essential for reactive applications. The decision comes down to your architecture:
- Spring MVC + synchronous needs = RestClient
- Spring WebFlux + reactive needs = WebClient
And if you’re on Java 21+, virtual threads tip the scales further toward RestClient. You get high-concurrency simplicity without the reactive learning curve.
Stop using WebClient with .block(). Start using RestClient for what it’s designed for.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 Spring Framework Rest Clients Documentation
- 👨💻 Spring Framework WebClient Documentation
- 👨💻 Reddit: WebClient for Synchronous Calls Discussion
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments