Backend Development 16 min read

Why RestTemplate Is Deprecated and How to Migrate to Spring WebClient

This article explains why Spring's RestTemplate has been deprecated, outlines the advantages of the non‑blocking WebClient, and provides detailed migration examples—including GET, POST, error handling, streaming, retries, custom configurations, and best‑practice guidelines for using WebClient in backend development.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Why RestTemplate Is Deprecated and How to Migrate to Spring WebClient

For many years, Spring's RestTemplate has been the default choice for client‑side HTTP access, offering a simple synchronous, blocking API. However, the growing demand for non‑blocking, reactive programming—especially in microservice architectures—has exposed RestTemplate's limitations. Since Spring Framework 5, RestTemplate is marked as deprecated, and the Spring team recommends WebClient as its successor.

1. Why RestTemplate Is Deprecated

Blocking nature: RestTemplate is a blocking, synchronous client. The thread that executes a request remains blocked until the operation completes, which can exhaust thread pools and increase latency under heavy load. This model does not scale well for microservices that must handle thousands of concurrent requests.

Limited scalability: The synchronous behavior restricts throughput and latency improvements. Modern systems that require high throughput and low latency favor event‑driven, reactive paradigms, leading to the adoption of non‑blocking APIs such as WebClient.

Lack of reactive support: RestTemplate does not support reactive programming, which is increasingly important in cloud‑native ecosystems. Reactive programming provides better responsiveness, resilience, and elasticity—capabilities that RestTemplate's blocking model cannot deliver.

2. The Rise of WebClient

WebClient is part of the Spring WebFlux library introduced with Spring 5. Compared with RestTemplate, it offers several advantages:

Non‑blocking operations: WebClient runs on Project Reactor's non‑blocking, reactive foundation, allowing it to handle concurrency with fewer threads and lower overhead.

Reactive stack: It integrates with the reactive stack, making it suitable for event‑loop‑based runtimes and high‑concurrency microservice scenarios.

JSON handling and more: WebClient uses Jackson for seamless JSON integration and also supports Server‑Sent Events (SSE), streaming, and other advanced use cases.

3. Migrating from RestTemplate to WebClient

Executing a GET request

RestTemplate:

RestTemplate restTemplate = new RestTemplate();
ResponseEntity
response = restTemplate.getForEntity("http://example.com", String.class);

WebClient:

WebClient webClient = WebClient.create();
Mono
response = webClient.get()
    .uri("http://example.com")
    .retrieve()
    .bodyToMono(String.class);
response.subscribe(result -> System.out.println(result));

In WebClient the retrieve() method initiates the request, bodyToMono converts the response body to a Mono , and subscribe() processes the result when it becomes available.

Error handling

RestTemplate uses an ErrorHandler interface, requiring separate code blocks. WebClient simplifies error handling with fluent operators.

WebClient example:

WebClient webClient = WebClient.create();
webClient.get()
    .uri("http://example.com/some-error-endpoint")
    .retrieve()
    .onStatus(HttpStatus::isError, response -> {
        // handle error status code
        return Mono.error(new CustomException("Custom error occurred."));
    })
    .bodyToMono(String.class);

POST request with JSON

RestTemplate:

RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity
request = new HttpEntity<>("{\"key\":\"value\"}", headers);
ResponseEntity
response = restTemplate.postForEntity("http://example.com", request, String.class);

WebClient:

WebClient webClient = WebClient.create();
Mono
response = webClient.post()
    .uri("http://example.com")
    .contentType(MediaType.APPLICATION_JSON)
    .bodyValue("{\"key\":\"value\"}")
    .retrieve()
    .bodyToMono(String.class);

WebClient makes header and body configuration more concise.

Asynchronous processing

WebClient excels at asynchronous calls, allowing multiple independent API calls to run concurrently.

WebClient webClient = WebClient.create();
Mono
responseOne = webClient.get()
    .uri("http://example.com/endpointOne")
    .retrieve()
    .bodyToMono(String.class);

Mono
responseTwo = webClient.get()
    .uri("http://example.com/endpointTwo")
    .retrieve()
    .bodyToMono(String.class);

Mono.zip(responseOne, responseTwo).subscribe(results -> {
    System.out.println("Result 1: " + results.getT1());
    System.out.println("Result 2: " + results.getT2());
});

Streaming data

WebClient can retrieve response bodies as a data stream, useful for large payloads that should not be fully loaded into memory.

WebClient webClient = WebClient.create();
webClient.get()
    .uri("http://example.com/stream")
    .accept(MediaType.TEXT_EVENT_STREAM)
    .retrieve()
    .bodyToFlux(String.class)
    .subscribe(data -> System.out.println("Received: " + data));

Retry mechanism

WebClient webClient = WebClient.builder()
    .baseUrl("http://example.com")
    .build();
Mono
response = webClient.get()
    .uri("/retry-endpoint")
    .retrieve()
    .bodyToMono(String.class)
    .retryWhen(Retry.backoff(3, Duration.ofSeconds(1))
    .onErrorResume(e -> Mono.just("Fallback response"));
response.subscribe(result -> System.out.println(result));

Custom client configuration

// Build a WebClient with specific timeouts and default headers
WebClient customWebClient = WebClient.builder()
    .baseUrl("http://example.com")
    .clientConnector(new ReactorClientHttpConnector(
        HttpClient.create()
            .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 2000)
            .responseTimeout(Duration.ofSeconds(2))
            .doOnConnected(conn -> conn
                .addHandlerLast(new ReadTimeoutHandler(2))
                .addHandlerLast(new WriteTimeoutHandler(2)))))
    .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
    .defaultHeader("Another-Header", "Another-Value")
    .build();

Network client filters

// WebClient with a logging filter
WebClient filteredWebClient = WebClient.builder()
    .baseUrl("http://example.com")
    .filter((request, next) -> {
        System.out.println("Request: " + request.method() + " " + request.url());
        return next.exchange(request).doOnSuccessOrError((response, error) -> {
            if (response != null) {
                System.out.println("Response status: " + response.statusCode());
            }
            if (error != null) {
                System.out.println("Error: " + error.getMessage());
            }
        });
    })
    .build();

Mutual TLS authentication

// Prepare SSL context with trust store and key store (demo only)
SslContext sslContext = SslContextBuilder.forClient()
    .trustManager(InsecureTrustManagerFactory.INSTANCE)
    .keyManager(new File("path/to/client.crt"), new File("path/to/client.key"))
    .build();

WebClient secureWebClient = WebClient.builder()
    .clientConnector(new ReactorClientHttpConnector(
        HttpClient.create().secure(sslContextSpec -> sslContextSpec.sslContext(sslContext))))
    .build();

4. WebClient Best Practices

Singleton usage

Unlike RestTemplate, which is often instantiated per request, WebClient is designed to be used as a singleton. Define a single WebClient.Builder bean and reuse it throughout the application to avoid unnecessary resource overhead.

@Bean
public WebClient.Builder webClientBuilder() {
    return WebClient.builder();
}

Error handling

Reactive streams propagate errors downstream until they are handled. Use operators such as onErrorResume , onErrorReturn , and doOnError to manage failures gracefully.

webClient.get()
    .uri("/endpoint")
    .retrieve()
    .bodyToMono(String.class)
    .doOnError(e -> log.error("Error occurred", e))
    .onErrorResume(e -> Mono.just("Fallback value"));

Timeout configuration

Always configure a timeout; otherwise a request may hang indefinitely if the server does not respond.

webClient.get()
    .uri("/endpoint")
    .retrieve()
    .bodyToMono(String.class)
    .timeout(Duration.ofSeconds(10));

Back‑pressure

When dealing with Flux streams, be aware of back‑pressure and use operators like limitRate to control the flow of data.

Logging

Enable detailed logging by setting the logger for reactor.netty.http.client.HttpClient to DEBUG, which provides request and response details.

Thread context

Reactive pipelines may switch threads multiple times. If you rely on thread‑local variables, consider using libraries such as reactor-context to propagate context across threads.

Avoid blocking calls

The main advantage of WebClient is its non‑blocking nature. If a blocking call is unavoidable, offload it with subscribeOn(Schedulers.boundedElastic()) .

Mono.fromCallable(() -> blockingMethod())
    .subscribeOn(Schedulers.boundedElastic());

WebClient provides a modern, non‑blocking, reactive way to perform HTTP requests, making it the preferred choice over the deprecated RestTemplate. By following the best‑practice guidelines and understanding reactive patterns, developers can fully leverage WebClient to build efficient, scalable, and responsive backend applications.

Javabackend developmentSpringReactive ProgrammingRestTemplateWebClient
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.