RadhaPublication
HomeInterviewLearning PathsRole TracksMockBooksPricing
Login / Sign UpLog in

© 2026 Radha Publication. All rights reserved.

Shop on AmazonDonateContactPrivacyTerms
Spring Boot questions
MediumSpring Boot#557 min readJul 11, 2026

WebClient vs RestTemplate.

practice
learning
Practice modeTest yourself instead of reading straight through

Hook: Interviewers love this question because it checks whether you can spot the difference between waiting for a response and letting the request flow without blocking a thread.

Question: WebClient vs RestTemplate.

Answer: RestTemplate is the classic synchronous HTTP client: the calling thread waits until the remote server replies. WebClient is the newer reactive client: it can send a request without tying up a thread while it waits, which makes it a better fit for high concurrency, streaming, and modern reactive services.

Interview-Ready Answer: “I’d use RestTemplate for simple blocking calls in older or servlet-based code, but for new Spring Boot services I prefer WebClient. The big difference is that RestTemplate blocks a request thread while waiting on the remote service, while WebClient is non-blocking and fits reactive pipelines. In practice, both can call REST endpoints, but WebClient scales better when you have many concurrent calls, streaming, or need to compose async flows cleanly.”

🧠 Memory Map
Memory map — visual summary of this topic

What they are

RestTemplate is the older, imperative HTTP client in Spring. It is simple and familiar: call a method, wait, get a response. WebClient is the reactive HTTP client from Spring WebFlux. Reactive means the code is built around asynchronous signals instead of a thread sitting idle; Mono means 0 or 1 result, and Flux means 0 to many results.

How they work under the hood

  1. You build an HTTP request: method, URL, headers, body.
  2. With RestTemplate, Spring serializes the body through HttpMessageConverters, sends the request through a chosen client request factory, and then blocks the current thread until the response arrives.
  3. With WebClient, Spring builds a reactive pipeline first and returns a Mono or Flux immediately. The actual network I/O is handled later by a non-blocking runtime such as Reactor Netty, which uses a small set of event-loop threads (threads that keep many sockets moving without parking one thread per request).
  4. When the response bytes arrive, WebClient decodes them with codecs (body readers/writers) and completes the reactive signal. If you call block(), you force the current thread to wait again, which removes most of the scaling advantage.
  5. If the response is 4xx/5xx, RestTemplate throws a RestClientResponseException subclass, while WebClient turns that into an error signal unless you customize it with onStatus or exchangeToMono.

Side-by-side comparison

AspectRestTemplateWebClient
StyleBlockingNon-blocking
Return typeObject / ResponseEntityMono / Flux
Best fitServlet apps, legacy codeReactive apps, high concurrency
StreamingPoor fitStrong fit
Error handlingExceptionsError signals
Thread usageOne waiting thread per callFew event-loop threads

When to use each one

  • Use RestTemplate when you have existing MVC code, a small number of outbound calls, and you want the simplest mental model.
  • Use WebClient when you are building new services, expect many concurrent requests, or need reactive composition, retries, or streaming responses.
  • You can use WebClient in a servlet app too; it does not force your whole application to become reactive.

Performance and practical numbers

  • CPU work per request is roughly O(1) for both; the real cost is network latency.
  • The big difference is thread cost. If 200 requests each wait 300 ms on a downstream service, RestTemplate may tie up about 200 request threads during that wait, while WebClient can keep that waiting work off the caller thread.
  • In production, common starting points are a connect timeout of 1-2 seconds, a response timeout of 3-5 seconds, and a connection pool sized to traffic, often somewhere in the 100-500 range depending on downstream speed and QPS.
  • Memory-wise, blocking threads are expensive because every live thread consumes stack space and scheduler overhead. Reactive code usually uses fewer threads, so it behaves better under bursts.

Important edge cases

  • WebClient is not automatically better if you immediately call block() everywhere; that turns it back into a blocking style.
  • Do not call block() on a reactive event-loop thread; that can cause starvation and ugly latency spikes.
  • RestTemplate is still supported, but it is not the recommended choice for new reactive systems.
  • Always configure timeouts. A missing timeout can make a bug look like a mystery outage when the downstream service is slow or unreachable.

Memory check: think “RestTemplate = the thread waits in line; WebClient = take a number and walk away.”

Real-World Story: Imagine a checkout service in an e-commerce app. Every checkout request calls pricing, inventory, and shipping services before charging the card. If the team uses RestTemplate everywhere and one downstream service gets slow, each incoming checkout thread sits blocked. Under load, Tomcat threads fill up, new requests queue, and users see spinning loaders or 502 errors. Logs often show timeouts like Read timed out or connection pool exhaustion, and the blast radius is immediate: carts cannot complete, revenue drops, and support tickets spike. The same system with WebClient can absorb more in-flight calls, but only if the team keeps it non-blocking; if someone adds .block() inside a reactive flow, the app can stall in a different way, with event-loop starvation and very slow responses that are hard to debug.

Spring Boot
package com.example.webclientvresttemplate;

import java.time.Duration;

import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;

@SpringBootApplication
public class WebClientVsRestTemplateApplication {

    public static void main(String[] args) {
        SpringApplication.run(WebClientVsRestTemplateApplication.class, args);
    }

    @Bean
    RestTemplate restTemplate(RestTemplateBuilder builder) {
        // Timeouts matter: without them, a slow downstream can hold a thread far longer than you expect.
        return builder
                .setConnectTimeout(Duration.ofSeconds(2))
                .setReadTimeout(Duration.ofSeconds(3))
                .build();
    }

    @Bean
    WebClient webClient() {
        // This demo calls the local server. In real systems, put the downstream base URL in configuration.
        return WebClient.builder()
                .baseUrl("http://localhost:8080")
                .build();
    }

    @Bean
    ApplicationRunner demoRunner(RestTemplate restTemplate, WebClient webClient) {
        return args -> {
            System.out.println("=== RestTemplate success ===");
            callWithRestTemplate(restTemplate, 1);

            System.out.println("=== RestTemplate 404 edge case ===");
            callWithRestTemplate(restTemplate, 404);

            System.out.println("=== WebClient success ===");
            callWithWebClient(webClient, 2);

            System.out.println("=== WebClient 404 edge case ===");
            callWithWebClient(webClient, 404);
        };
    }

    private void callWithRestTemplate(RestTemplate restTemplate, int id) {
        try {
            ResponseEntity<UserDto> response = restTemplate.getForEntity(
                    "http://localhost:8080/api/users/{id}",
                    UserDto.class,
                    id);

            System.out.println("RestTemplate status=" + response.getStatusCode() + ", body=" + response.getBody());
        } catch (HttpClientErrorException.NotFound ex) {
            // RestTemplate turns a 404 into an exception, so you must catch it if 404 is an expected case.
            System.out.println("RestTemplate got 404 as expected: " + ex.getStatusCode());
        }
    }

    private void callWithWebClient(WebClient webClient, int id) {
        try {
            UserDto user = webClient.get()
                    .uri("/api/users/{id}", id)
                    .retrieve()
                    .bodyToMono(UserDto.class)
                    // block() is fine in this tiny demo runner because we are outside a reactive request pipeline.
                    // In a reactive controller or handler, blocking would waste the non-blocking advantage.
                    .block(Duration.ofSeconds(2));

            System.out.println("WebClient body=" + user);
        } catch (WebClientResponseException.NotFound ex) {
            // retrieve() treats 4xx/5xx as errors by default, which is usually what you want for API calls.
            System.out.println("WebClient got 404 as expected: " + ex.getStatusCode());
        }
    }
}

@RestController
class UserController {

    @GetMapping("/api/users/{id}")
    ResponseEntity<UserDto> getUser(@PathVariable int id) {
        if (id == 404) {
            return ResponseEntity.notFound().build();
        }
        return ResponseEntity.ok(new UserDto(id, "User-" + id));
    }
}

record UserDto(int id, String name) { }

Follow-up & Tricky Questions:

  • How does error handling differ? RestTemplate throws exceptions such as HttpClientErrorException and HttpServerErrorException. WebClient gives you a reactive error signal, and you can map status codes with onStatus or exchangeToMono.
  • Can I use WebClient in a Spring MVC app? Yes. It works fine in servlet-based apps, and that is a common migration path when you want a better client without rewriting the whole application to reactive.
  • When would you still choose RestTemplate? For simple blocking code, small internal tools, or older codebases where the team is not using reactive programming. It is perfectly acceptable when the load is modest and the codebase is already imperative.
  • What is backpressure? Backpressure means the consumer can signal that it cannot keep up, so the producer slows down. That matters for streaming with Flux, and it is one reason reactive systems handle large streams more safely.
  • How do retries and timeouts fit in? Both clients can be used with timeouts, and retries should be applied carefully only for safe operations. In microservices, retrying every failure can make an outage worse if the downstream service is already overloaded.
  • Is WebClient always faster? Not always. It is usually more scalable under concurrency, but a simple low-traffic blocking call can be just as fast or easier to maintain with RestTemplate.
  • Does WebClient require reactive controllers? No. You can use it from ordinary service classes too; just avoid blocking inside reactive request paths if you want the non-blocking benefit.
  • Can I replace RestTemplate with WebClient by adding .block()? You can, but that is only a partial migration. It may compile, but you are still paying the blocking cost, so the real scalability gain is lost.

Common Mistakes:

  • Using WebClient and calling .block() everywhere. That erases most of the reactive benefit. Use reactive chains when you actually want non-blocking behavior.
  • Leaving timeouts unset. A missing timeout turns a temporary downstream issue into threads waiting forever. Always set connect and read/response timeouts.
  • Thinking RestTemplate is “broken.” It is still fine for many servlet-based services. The better question is whether your workload needs non-blocking scalability.
  • Assuming WebClient automatically streams. It can stream very well, but only if you use the right return types such as Flux and the endpoint supports streaming.

Memory Hook: RestTemplate is the thread waiting in a lobby; WebClient is the thread taking a pager and walking away.

Cheat Sheet:

  • RestTemplate = blocking, simple, synchronous.
  • WebClient = non-blocking, reactive, scalable.
  • Use RestTemplate for legacy MVC or small workloads.
  • Use WebClient for new code, high concurrency, and streaming.
  • Set timeouts and handle 4xx/5xx explicitly.
  • Never block on reactive event-loop threads.

Practice Tasks:

  • Replace one RestTemplate call in an existing service with WebClient and keep the same response shape.
  • Add a 2-second timeout and test what happens when the downstream endpoint sleeps for 5 seconds.
  • Change the demo to return a list as Flux<UserDto> and observe how streaming differs from a normal single-object response.
Previous
Back to Questions←→to navigate
Next

Recommended Resources

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

package com.example.webclientvresttemplate; import java.time.Duration; import org.springframework.boot.ApplicationRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.annotation.Bean; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClientResponseException; @SpringBootApplication public class WebClientVsRestTemplateApplication { public static void main(String[] args) { SpringApplication.run(WebClientVsRestTemplateApplication.class, args); } @Bean RestTemplate restTemplate(RestTemplateBuilder builder) { // Timeouts matter: without them, a slow downstream can hold a thread far longer than you expect. return builder .setConnectTimeout(Duration.ofSeconds(2)) .setReadTimeout(Duration.ofSeconds(3)) .build(); } @Bean WebClient webClient() { // This demo calls the local server. In real systems, put the downstream base URL in configuration. return WebClient.builder() .baseUrl("http://localhost:8080") .build(); } @Bean ApplicationRunner demoRunner(RestTemplate restTemplate, WebClient webClient) { return args -> { System.out.println("=== RestTemplate success ==="); callWithRestTemplate(restTemplate, 1); System.out.println("=== RestTemplate 404 edge case ==="); callWithRestTemplate(restTemplate, 404); System.out.println("=== WebClient success ==="); callWithWebClient(webClient, 2); System.out.println("=== WebClient 404 edge case ==="); callWithWebClient(webClient, 404); }; } private void callWithRestTemplate(RestTemplate restTemplate, int id) { try { ResponseEntity<UserDto> response = restTemplate.getForEntity( "http://localhost:8080/api/users/{id}", UserDto.class, id); System.out.println("RestTemplate status=" + response.getStatusCode() + ", body=" + response.getBody()); } catch (HttpClientErrorException.NotFound ex) { // RestTemplate turns a 404 into an exception, so you must catch it if 404 is an expected case. System.out.println("RestTemplate got 404 as expected: " + ex.getStatusCode()); } } private void callWithWebClient(WebClient webClient, int id) { try { UserDto user = webClient.get() .uri("/api/users/{id}", id) .retrieve() .bodyToMono(UserDto.class) // block() is fine in this tiny demo runner because we are outside a reactive request pipeline. // In a reactive controller or handler, blocking would waste the non-blocking advantage. .block(Duration.ofSeconds(2)); System.out.println("WebClient body=" + user); } catch (WebClientResponseException.NotFound ex) { // retrieve() treats 4xx/5xx as errors by default, which is usually what you want for API calls. System.out.println("WebClient got 404 as expected: " + ex.getStatusCode()); } } } @RestController class UserController { @GetMapping("/api/users/{id}") ResponseEntity<UserDto> getUser(@PathVariable int id) { if (id == 404) { return ResponseEntity.notFound().build(); } return ResponseEntity.ok(new UserDto(id, "User-" + id)); } } record UserDto(int id, String name) { }