Appearance
Service-to-Service Calls with RestTemplate
A microservice application is several small Spring Boot applications that work together. The first thing they must be able to do is talk to each other. This chapter sets up two services and makes one call the other: first in plain Java, to see every step, then with Spring's RestTemplate, which hides those steps.
Step 1: two services on two ports
Create two projects on start.spring.io, each with only the Spring Web dependency:
- order-service, which will make the call
- product-service, which will answer it
Two applications on one machine cannot share a port, so give each its own in application.properties:
properties
# order-service
server.port=8081properties
# product-service
server.port=8082Start both. The logs show Tomcat started on port 8081 and Tomcat started on port 8082.
product-service exposes one endpoint:
java
@RestController
@RequestMapping("/products")
public class ProductController {
@GetMapping("/{id}")
public String getProduct(@PathVariable String id) {
return "Product fetched with id: " + id;
}
}The goal: when someone calls GET /orders/{id} on order-service, it calls GET /products/{id} on product-service.
Synchronous and asynchronous communication
Services can talk in two ways:
- Synchronous: the client waits for the server's response before it continues. It is blocking: the calling thread does nothing until the response arrives.
- Asynchronous: the client sends a message (usually through a broker) and carries on.
This chapter and the next two cover synchronous communication. Spring offers three tools for it:
- RestTemplate: the older, traditional client (this chapter).
- RestClient: its modern replacement (chapter 2).
- FeignClient: the Spring Cloud client (chapter 3).
FeignClient is what you use once the services join Spring Cloud, because it integrates with service discovery, load balancing and the resilience patterns of later chapters. It is built on the same ideas as the first two, so we start with those.
What travels over the wire
Every tool in this chapter builds and sends the same thing: an HTTP request.
A GET request:
http
GET /products/1 HTTP/1.1
Host: localhost:8082
User-Agent: curl/8.7.1
Accept: application/json| Part | Meaning |
|---|---|
GET | the HTTP method (GET, POST, PUT, DELETE...) |
/products/1 | the path (URI) on the server |
HTTP/1.1 | the protocol version (1.0, 1.1, 2, 3) |
Host | the target host: host name and port of the server |
User-Agent | the client or tool making the request (curl, Postman, Java...) |
Accept | the format the client wants the response in |
A POST request also carries a body:
http
POST /products HTTP/1.1
Host: localhost:8082
Accept: application/json
Content-Type: application/json
Content-Length: 35
{"name": "Ice-cream", "price": 200}Content-Type tells the server the body is JSON; Content-Length is the body's size in bytes.
A response:
http
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 31
Connection: keep-alive
Keep-Alive: timeout=5, max=50
{"id": 10, "name": "Ice-cream"}The status code says how it went: 2xx success, 4xx the client sent a bad request, 5xx the server failed. Content-Type and Content-Length now describe the response body. The two connection headers deserve their own section.
Keep-alive: reusing a TCP connection
HTTP runs over a TCP connection. Opening one costs a 3-way handshake (SYN, SYN-ACK, ACK) and closing one a 4-way termination (FIN, ACK, FIN, ACK). Doing that for every request is wasteful.
- In HTTP/1.0 the default is
Connection: close: after each response the TCP connection is closed and the next request opens a new one. - In HTTP/1.1 the default is
Connection: keep-alive: the connection stays open and the next request to the same server reuses it.
The Keep-Alive header sets the limits:
timeout=5: close the connection once it has been idle for 5 seconds.max=50: send at most 50 requests over this connection.
With Connection: close, the termination follows every single response. Keep this in mind: the code below is shaped around reusing connections.
Step 2: the call in plain Java
Before any Spring help, here is what it takes with only the JDK's HttpURLConnection. In order-service:
java
@RestController
@RequestMapping("/orders")
public class OrderController {
@GetMapping("/{id}")
public ResponseEntity<String> getOrder(@PathVariable String id) {
HttpURLConnection connection = null;
try {
URL url = URI.create("http://localhost:8082/products/" + id).toURL();
// 1. Build the request: no network traffic yet
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Accept", "application/json");
connection.setConnectTimeout(100); // max ms to open the TCP connection
connection.setReadTimeout(500); // max ms to wait for the response
// 2. Connect, send, receive: getInputStream() does all three
StringBuilder response = new StringBuilder();
try (BufferedReader in = new BufferedReader(
new InputStreamReader(connection.getInputStream()))) {
String line;
while ((line = in.readLine()) != null) {
response.append(line);
}
}
System.out.println("Response: " + response);
} catch (IOException e) {
// handle the failure
} finally {
// 3. Release the connection
if (connection != null) {
connection.disconnect();
}
}
return ResponseEntity.ok("order call successful");
}
}Calling GET localhost:8081/orders/1 returns order call successful, and order-service prints Response: Product fetched with id: 1.
Walk through the three parts.
1. Build the request. openConnection() does not connect to anything. It creates an HttpURLConnection object: think of it as an envelope on which you write the method, headers, URL and timeouts.
- Connect timeout: how long to wait for the TCP connection to be established.
- Read timeout: once connected and the request is sent, how long to wait for the response. (The call is blocking, so without a limit the thread could wait forever.)
2. Connect, send, receive. The real TCP connection is made by the first I/O call on the object: connect(), getInputStream() or getResponseCode(). getInputStream() connects, sends the request and returns the response body as a stream, which we read line by line.
When connecting, Java does not blindly open a new socket. It first checks the KeepAliveCache, a map from host:port to an open connection (an internal HttpClient object that wraps the TCP socket). If a live connection to localhost:8082 is there, it is reused; otherwise a new one is opened.
3. Release. Once the response body has been read completely and the stream closed, the connection goes back to the KeepAliveCache for the next request. If the body was not read fully (say an exception interrupted it), the connection is in an unknown state and is closed instead.
You can see the reuse. Make product-service return request.getRemotePort() (the client's port) and call it three times in a row from order-service: all three requests arrive from the same client port, so they share one TCP connection.
Why this is not enough
- Boilerplate: opening the connection, setting headers, reading the stream, closing everything, for every call.
- Manual response handling: the body arrives as raw text. Mapping JSON to a
Productobject is your job. - No advanced features: connection pooling, interceptors and similar must be built by hand.
Step 3: the same call with RestTemplate
RestTemplate wraps all of the above. Create it once as a bean in order-service:
java
@Configuration
public class AppConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}Then inject it and call product-service:
java
@RestController
@RequestMapping("/orders")
public class OrderController {
@Autowired
RestTemplate restTemplate;
@GetMapping("/{id}")
public ResponseEntity<String> getOrder(@PathVariable String id) {
String response = restTemplate.getForObject(
"http://localhost:8082/products/" + id, String.class);
System.out.println("Response from Product API called from order service: " + response);
return ResponseEntity.ok("order call successful");
}
}The whole HttpURLConnection block became one line. getForObject takes the URL and the type the body should be converted to (String.class here, or a class like Product.class for a JSON body).
Setting timeouts
new RestTemplate() uses default timeouts (none: it can wait indefinitely). To set them, configure a request factory and pass it to the constructor:
java
@Bean
public RestTemplate restTemplate() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(1000); // ms
factory.setReadTimeout(5000); // ms
return new RestTemplate(factory);
}Forgetting the argument (return new RestTemplate();) is a common slip: the factory is built and then thrown away, and the timeouts never apply.
How RestTemplate works inside
RestTemplate performs the same three steps as the plain Java code. Its default request factory is that same SimpleClientHttpRequestFactory, built on HttpURLConnection.
getForObjectasks the request factory to create a request.- The factory creates the
HttpURLConnection(the envelope), sets method and timeouts, and returns it wrapped in aSimpleClientHttpRequest. RestTemplatecallsexecute()on that request.execute()callsconnect(), which checks theKeepAliveCachefor the target host:port.getResponseCode()sends the request and waits for the response.- The response is wrapped in a
SimpleClientHttpResponse. RestTemplateconverts the body to the requested type with its message converters (Jackson for JSON).- It closes the response stream, not the TCP connection, so the connection returns to the cache and the next call to product-service reuses it.
Everything you wrote by hand, the framework now does.
The other RestTemplate methods
| Method | Use |
|---|---|
getForObject(url, Type.class) | GET, return the body only |
getForEntity(url, Type.class) | GET, return the full ResponseEntity (status, headers, body) |
postForObject(url, body, Type.class) | POST a body, return the response body |
postForEntity(url, body, Type.class) | POST a body, return the full ResponseEntity |
put(url, body) | PUT; returns nothing |
delete(url) | DELETE; returns nothing |
exchange(url, method, entity, Type.class) | any method, your own headers and body, automatic conversion |
execute(url, method, callback, extractor) | full manual control |
The ...ForObject and ...ForEntity pairs differ only in what they return:
java
Product product = restTemplate.getForObject(url, Product.class);
ResponseEntity<Product> response = restTemplate.getForEntity(url, Product.class);
HttpStatusCode status = response.getStatusCode();
Product body = response.getBody();java
Product created = restTemplate.postForObject(url, new Product("Ice-cream", 100), Product.class);
restTemplate.put(url + "/1", new Product("Ice-cream", 150));
restTemplate.delete(url + "/1");exchange: your headers, Spring's conversion
Use exchange when you need to set headers (an Authorization token, say) but still want Spring to serialize the body and convert the response:
java
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "Bearer my-token");
Product product = new Product("Ice-cream", 100);
HttpEntity<Product> request = new HttpEntity<>(product, headers); // body + headers
ResponseEntity<Product> response = restTemplate.exchange(
url, HttpMethod.POST, request, Product.class);HttpEntity holds the body and the headers together.
execute: everything by hand
execute gives you the raw request and response, like HttpURLConnection, through two functional interfaces:
RequestCallback:void doWithRequest(ClientHttpRequest request): set headers, write the body.ResponseExtractor<T>:T extractData(ClientHttpResponse response): read and convert the body.
java
RequestCallback requestCallback = request -> {
request.getHeaders().setContentType(MediaType.APPLICATION_JSON);
byte[] body = new ObjectMapper().writeValueAsBytes(new Product("Ice-cream", 100));
StreamUtils.copy(body, request.getBody()); // you serialize the body
};
ResponseExtractor<String> responseExtractor = response ->
StreamUtils.copyToString(response.getBody(), StandardCharsets.UTF_8); // you parse it
String response = restTemplate.execute(url, HttpMethod.POST, requestCallback, responseExtractor);All the other methods are built on execute; they just supply their own callback and extractor.
Errors
For a 4xx or 5xx response, RestTemplate throws an exception instead of returning: HttpClientErrorException for 4xx (for example HttpClientErrorException.NotFound for 404) and HttpServerErrorException for 5xx. A read timeout throws ResourceAccessException.
Why RestTemplate is "legacy"
- Too many overloaded methods. Each method in the table has several overloads (URL as
StringorURI, variables as varargs or aMap...). They are hard to remember and hard to maintain. - Built before modern needs. Retries, circuit breakers and interceptors arrived later; each new feature would mean more overloads across every method.
- Maintenance mode. Spring accepts only bug fixes for it, no new features.
Its successor, RestClient, uses a fluent, builder-style API: one chain of get(), uri(), retrieve(), body() instead of dozens of overloads, with easy hooks for interceptors and filters.
Next: Chapter 2 rewrites this same order-to-product call with RestClient, and shows how its fluent API and its pooled HTTP clients improve on RestTemplate.
Check yourself
- What makes a call synchronous, and what happens to the calling thread?
- What do the
HostandAcceptrequest headers say? - What does
Keep-Alive: timeout=5, max=50mean? - When does
HttpURLConnectionactually open a TCP connection? - What is the
KeepAliveCache, and what are its keys and values? - When does a connection go back to the cache, and when is it closed instead?
- What is wrong with this bean?java
SimpleClientHttpRequestFactory f = new SimpleClientHttpRequestFactory(); f.setReadTimeout(5000); return new RestTemplate(); - What is the difference between
getForObjectandgetForEntity? - When would you use
exchange, and whenexecute? - Give two reasons RestTemplate is considered legacy.
Answers
- The client waits for the server's response before continuing. The thread is blocked until the response arrives (or a timeout fires).
Hostnames the target host and port;Acceptnames the response format the client wants.- Close the TCP connection once it has been idle for 5 seconds, and send at most 50 requests over it.
- On the first I/O call:
connect(),getInputStream()orgetResponseCode().openConnection()only creates the request object. - A cache of open TCP connections: the key is host:port, the value is the
HttpClientobject wrapping the socket. Before connecting, Java looks here for a connection to reuse. - It goes back once the response body has been fully read and the stream closed. If the body was not read fully, the connection is closed.
- The factory is never passed to
RestTemplate, so the timeout is ignored. Writereturn new RestTemplate(f);. getForObjectreturns only the converted body;getForEntityreturns aResponseEntitywith the status code, headers and body.exchangewhen you need your own method, headers and body but want automatic conversion;executewhen you want to write the request and read the response yourself.- Too many overloaded methods to remember and maintain; it predates retries, circuit breakers and interceptors; it is in maintenance mode (bug fixes only).