Spring Boot 4 finally forced the issue: RestTemplate, in maintenance mode since Spring 5, is out of the recommended path, and codebases full of getForObject() and exchange() calls need a plan. The good news is that RestClient (introduced in Spring 6.1 / Boot 3.2) was designed as the synchronous successor, so this migration is mostly mechanical — if you know the method-by-method mapping and the three places where behaviour quietly differs. This guide gives you both.
Tested with: Spring Boot 3.4 and 4.0, Java 21. If you are doing this as part of a larger upgrade, start with the Spring Boot 3 to 4 migration guide.
Why RestClient and Not WebClient?
| RestTemplate | RestClient | WebClient | |
|---|---|---|---|
| Programming model | Template methods | Fluent, synchronous | Fluent, reactive |
| Blocking? | Yes | Yes | No (Mono/Flux) |
| Extra dependency | None | None | spring-webflux |
| Status in Boot 4 | Maintenance / discouraged | Recommended default | For reactive stacks |
| Virtual-thread friendly | Yes | Yes | Unnecessary overhead |
For a servlet application — especially on Java 21 with virtual threads — RestClient is the correct target. Pulling in WebFlux just to make blocking calls through .block() was always the wrong trade, and RestClient removes the last excuse for it.
The Method Mapping Table
| RestTemplate | RestClient equivalent |
|---|---|
| getForObject(url, T.class) | get().uri(url).retrieve().body(T.class) |
| getForEntity(url, T.class) | get().uri(url).retrieve().toEntity(T.class) |
| postForObject(url, req, T.class) | post().uri(url).body(req).retrieve().body(T.class) |
| postForEntity(url, req, T.class) | post().uri(url).body(req).retrieve().toEntity(T.class) |
| put(url, req) | put().uri(url).body(req).retrieve().toBodilessEntity() |
| delete(url) | delete().uri(url).retrieve().toBodilessEntity() |
| exchange(url, method, entity, T.class) | method(httpMethod).uri(url).headers(…).body(…).retrieve().toEntity(T.class) |
| exchange(…, ParameterizedTypeReference) | …retrieve().body(new ParameterizedTypeReference<>() {}) |
Before and After
// BAD: RestTemplate (maintenance mode; out of the Boot 4 happy path)
RestTemplate restTemplate = new RestTemplate();
Student student = restTemplate.getForObject(
"https://api.example.com/students/{id}", Student.class, 42);
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
ResponseEntity<List<Student>> resp = restTemplate.exchange(
"https://api.example.com/students", HttpMethod.GET,
new HttpEntity<>(headers),
new ParameterizedTypeReference<List<Student>>() {});
// GOOD: RestClient (Boot 3.2+ / Boot 4)
@Service
public class StudentApiClient {
private final RestClient restClient;
// Inject the auto-configured builder — it carries Boot's message
// converters, observability, and customizers. Don't use RestClient.create()
public StudentApiClient(RestClient.Builder builder) {
this.restClient = builder
.baseUrl("https://api.example.com")
.build();
}
public Student findById(int id) {
return restClient.get()
.uri("/students/{id}", id)
.retrieve()
.body(Student.class);
}
public List<Student> findAll(String token) {
return restClient.get()
.uri("/students")
.headers(h -> h.setBearerAuth(token))
.retrieve()
.body(new ParameterizedTypeReference<List<Student>>() {});
}
}
The Three Behavioural Differences That Matter
1. Error Handling Defaults
Like RestTemplate, RestClient throws on 4xx/5xx by default — but the exception hierarchy and customization point changed. ResponseErrorHandler on the template becomes onStatus() per call or defaultStatusHandler() on the builder:
Student student = restClient.get()
.uri("/students/{id}", id)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, (req, resp) -> {
throw new StudentNotFoundException("No student " + id
+ " (HTTP " + resp.getStatusCode() + ")");
})
.body(Student.class);
2. exchange() Means Something Different Now
RestClient also has an exchange() method — but it is the low-level escape hatch, not the everyday call it was on RestTemplate. Crucially, RestClient’s exchange() disables the default status handlers: you take responsibility for checking the status yourself. Teams that mechanically rename restTemplate.exchange to restClient.exchange ship code that silently swallows 500s. Map old exchange() calls to retrieve() unless you genuinely need raw response access.
3. Interceptors and Timeouts Move to the Builder
@Bean
RestClient apiClient(RestClient.Builder builder) {
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults()
.withConnectTimeout(Duration.ofSeconds(2))
.withReadTimeout(Duration.ofSeconds(5)); // ALWAYS set timeouts
return builder
.baseUrl("https://api.example.com")
.requestFactory(ClientHttpRequestFactoryBuilder.detect().build(settings))
.requestInterceptor((request, body, execution) -> {
request.getHeaders().add("X-Request-Id", UUID.randomUUID().toString());
return execution.execute(request, body);
})
.build();
}
RestTemplate’s infinite default timeouts were a production incident generator; do not recreate them. Pair external calls with a Resilience4j circuit breaker for the failures timeouts alone can’t handle.
Migration Strategy for Large Codebases
Wrap, then migrate: introduce client classes (like StudentApiClient above) around each external API, port call sites to the wrapper incrementally, and keep RestTemplate beans alive until the last call site is gone. If your RestTemplate usage was already behind an interface, consider leapfrogging to HTTP interface clients (@HttpExchange) — Boot 4 makes declarative clients first-class, and the interface style eliminates this entire category of migration next time. For load-balanced calls via Eureka, @LoadBalanced RestClient.Builder works exactly like the old RestTemplate version — see the Eureka guide. Auth setups port cleanly too; my Basic Auth guide shows both styles.
Frequently Asked Questions
Is RestTemplate actually removed in Spring Boot 4? The class still exists in Spring Framework, but Boot 4 drops it from the recommended path and new auto-configuration investment goes to RestClient and HTTP interfaces. Treat it as frozen legacy.
Do I lose anything moving to RestClient? Almost nothing — RestClient reuses the same message converters and request factories underneath. The one genuine gap is code relying on subclassing RestTemplate itself; that pattern has no equivalent and wants redesign anyway.
RestClient or HTTP interfaces? Both are correct; HTTP interfaces sit on top of RestClient. Use interfaces for well-defined external APIs, raw RestClient for dynamic URLs or one-off calls.
AI Prompts for the Migration
Convert a Client Class
Migrate this RestTemplate-based class to RestClient: [paste class here]. Use the injected RestClient.Builder, map every exchange() call to retrieve() unless raw response access is needed, preserve ParameterizedTypeReference usages, and set explicit connect/read timeouts.
What it does: The full mechanical conversion with the exchange() trap and timeout hygiene built in.
When to use it: Per client class — the workhorse prompt of this migration.
Port Error Handling
Here is my custom ResponseErrorHandler and the RestTemplate calls using it: [paste here]. Reproduce the same behaviour with RestClient using defaultStatusHandler on the builder and onStatus per call, and flag any behaviour that cannot be reproduced exactly.
What it does: Converts the customization point that has no one-to-one rename.
When to use it: When your codebase centralized error handling on the template.
Find Risky Call Sites
Scan these classes for RestTemplate usage: [paste here]. Categorize each call site as trivial (direct mapping), risky (exchange() with status-dependent logic, custom interceptors, subclassing), or blocked (no RestClient equivalent), and order the migration accordingly.
What it does: Triage, so the dangerous 10% of call sites get human attention.
When to use it: Before estimating the migration.
Leapfrog to HTTP Interfaces
This RestTemplate client calls a single external API: [paste here]. Rewrite it as a Spring @HttpExchange interface with the Boot 4 registration, and note which call sites need signature changes.
What it does: Skips a generation where the API shape allows it.
When to use it: For stable, well-defined external APIs.
Add Resilience While You’re There
For this migrated RestClient class: [paste here], add Resilience4j circuit breaker and retry annotations with sensible defaults for an external API with [X] SLA, keeping retries strictly on idempotent methods.
What it does: Uses the migration as the moment to add the protection RestTemplate code usually lacked.
When to use it: As the final pass on each migrated client.
Conclusion
RestClient is the rare migration target that is genuinely better at everything RestTemplate did: same blocking model, same converters, fluent API, real timeout ergonomics. Migrate via wrapper classes, translate exchange() to retrieve() deliberately rather than by rename, set timeouts on every builder, and consider HTTP interfaces for your stable APIs so this is the last HTTP-client migration you do.
See Also
- Spring Boot 4 HTTP Service Clients (@HttpExchange)
- Spring Boot 3 to 4 Migration Guide
- Spring Boot RestTemplate with Basic Auth
- Resilience4j Circuit Breaker in Spring Boot
- Spring Cloud Netflix to Modern Alternatives