Skip to content

Spring Boot Microservices: Synchronous Communication using RestTemplate

The Telephone Exchange Analogy

Imagine two offices on opposite sides of a city that need to coordinate on orders. Office A (the order desk) must call Office B (the product warehouse) every time a customer places an order. The call is synchronous: the order desk representative picks up the phone, dials the number, and waits on the line until someone at the warehouse answers and gives a confirmation. Only after receiving that confirmation does the order desk move on to the next customer.

This is precisely how synchronous microservice communication works. One service places a blocking call to another and waits for the response before continuing any further work. No background threads, no callbacks — just a straight telephone call between two services.

In this chapter we set up two Spring Boot microservices — order-service and product-service — and explore three layers of synchronous communication: raw Java, then RestTemplate, and finally a deep look at what happens under the hood during every HTTP exchange.


Synchronous vs. Asynchronous Communication

DimensionSynchronousAsynchronous
BlockingYes — caller thread waitsNo — caller continues immediately
ProtocolHTTP/RESTMessage queues (Kafka, RabbitMQ)
LatencyAdded per downstream hopDecoupled latency
Spring toolsRestTemplate, RestClient, FeignClientSpring Kafka, Spring AMQP

For this chapter we focus exclusively on synchronous HTTP communication because it is the most common pattern when services need an immediate answer before proceeding.


Setting Up Two Microservices

Create two Spring Boot projects from Spring Initializr with only the Spring Web dependency for now.

order-serviceapplication.properties

properties
server.port=8081
spring.application.name=order-service

product-serviceapplication.properties

properties
server.port=8082
spring.application.name=product-service

Product Service Controller

java
@RestController
@RequestMapping("/product")
public class ProductController {

    @GetMapping("/{id}")
    public String getProduct(@PathVariable Long id) {
        return "Product fetched with id: " + id;
    }
}

The goal: order-service must call /product/{id} on product-service without hard coupling them at the class level.


Understanding the Raw HTTP Request

Before touching Spring abstractions it helps to understand what an HTTP GET request looks like at the wire level.

GET /product/1 HTTP/1.1
Host: localhost:8082
User-Agent: Java/17
Accept: application/json

And a POST request:

POST /product HTTP/1.1
Host: localhost:8082
Content-Type: application/json
Content-Length: 42

{"name":"Laptop","price":999.99}

The response carries status, headers, and a body:

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 35
Connection: keep-alive
Keep-Alive: timeout=5, max=50

Product fetched with id: 1

HTTP keep alive — Why It Matters for Your Code

HTTP 1.0 closes the TCP connection after every response. HTTP 1.1 defaults to Connection: keep-alive, meaning the same TCP socket can be reused for multiple requests. The keep-alive header carries two parameters:

  • timeout=5 — close the idle TCP connection after 5 seconds of inactivity.
  • max=50 — allow at most 50 requests over this single TCP connection.

Spring's HTTP client wrappers maintain a KeepAliveCache — a map from (host, port) to HTTP client objects — so that when you fire a second request to the same upstream service, no new TCP handshake is needed. The client object is marked "in use" during the request and returned to the cache after the response stream is fully read.


Communicating with Plain Java (HttpUrlConnection)

Before RestTemplate was introduced, Java developers had to wire everything manually.

java
// Step 1 — build the "envelope" (HttpUrlConnection)
URL url = new URL("http://localhost:8082/product/1");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Accept", "application/json");
connection.setConnectTimeout(3_000); // 3 s to establish TCP connection
connection.setReadTimeout(5_000);   // 5 s waiting for server response

// Step 2 — initiate the TCP connection and send the request
// getInputStream() internally calls connect(), which triggers the TCP handshake,
// sends the HTTP request, and opens the response stream.
try (InputStream is = connection.getInputStream();
     BufferedReader reader = new BufferedReader(new InputStreamReader(is))) {

    StringBuilder sb = new StringBuilder();
    String line;
    while ((line = reader.readLine()) != null) {
        sb.append(line);
    }
    System.out.println("Response: " + sb);
}

// Step 3 — disconnect (returns TCP connection to keep-alive cache if response was fully read)
connection.disconnect();

Disadvantages of this approach:

  1. Boilerplate — connection setup, header configuration, stream reading, stream closing.
  2. Manual response parsing — you receive an InputStream and must convert it to any Java type yourself.
  3. No connection pool management — the keep alive cache exists at the JDK level but you have no easy hook to configure pool size or eviction policies.
  4. No interceptors — adding cross cutting concerns (logging, auth headers) requires wrapping each call site.

RestTemplate — Spring's Abstraction

RestTemplate is Spring's legacy HTTP client. It wraps HttpUrlConnection (by default via SimpleClientHttpRequestFactory) and hides all the boilerplate seen above.

Bean Configuration

java
@Configuration
public class AppConfig {

    /**
     * Basic RestTemplate with default timeouts.
     * Use this when upstream SLAs are predictable.
     */
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    /**
     * RestTemplate with explicit connection and read timeouts.
     * connectTimeout — max time to establish the TCP connection.
     * readTimeout    — max time to wait for the server response once connected.
     */
    @Bean
    public RestTemplate timedRestTemplate() {
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setConnectTimeout(3_000); // milliseconds
        factory.setReadTimeout(5_000);
        return new RestTemplate(factory);
    }
}

Order Service Controller

java
@RestController
@RequestMapping("/order")
public class OrderController {

    private final RestTemplate restTemplate;

    public OrderController(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @GetMapping("/{id}")
    public String getOrder(@PathVariable Long id) {
        // Single line replaces ~25 lines of HttpUrlConnection boilerplate
        String response = restTemplate.getForObject(
            "http://localhost:8082/product/" + id,
            String.class
        );
        System.out.println("Response from product service: " + response);
        return "Order placed. " + response;
    }
}

In one line — getForObject(url, responseType) — Spring handles TCP connection (with keep alive caching), request serialization, response deserialization, and stream cleanup.


RestTemplate Internal Flow

Understanding the internals prevents surprises in production. The call chain for getForObject(url, String.class) is:

  1. createRequest() — invokes SimpleClientHttpRequestFactory.createRequest(), which creates an HttpUrlConnection object. Headers, method, and timeouts are applied.
  2. execute() — calls connection.connect(). Internally this consults the KeepAliveCache: if a live HTTP client object exists for localhost:8082, it is reused; otherwise a new TCP connection is established.
  3. getResponseCode() — sends the HTTP request and waits for the response. The response stream is now available inside the connection object.
  4. Response extractionRestTemplate wraps the connection in a SimpleClientHttpResponse and passes it through registered HttpMessageConverter instances. For String.class the StringHttpMessageConverter reads the byte stream and returns a String.
  5. Stream close — the response stream is closed. The underlying TCP connection is NOT closed; it is marked "available" back in the KeepAliveCache for future reuse.

RestTemplate Method Reference

GET

java
// Returns only the response body, deserialized to the given type
Product product = restTemplate.getForObject(url, Product.class);

// Returns the full ResponseEntity including status code and headers
ResponseEntity<Product> entity = restTemplate.getForEntity(url, Product.class);
HttpStatus status = entity.getStatusCode();
Product body = entity.getBody();

POST

java
Product newProduct = new Product("Laptop", 999.99);

// Returns only the response body
Product created = restTemplate.postForObject(url, newProduct, Product.class);

// Returns the full ResponseEntity
ResponseEntity<Product> response = restTemplate.postForEntity(url, newProduct, Product.class);

PUT

java
// PUT typically returns no body
restTemplate.put("http://localhost:8082/product/1", updatedProduct);

DELETE

java
restTemplate.delete("http://localhost:8082/product/1");

The exchange Method — Full Control over Headers

Use exchange when you need to set custom headers (e.g., an Authorization token) while still relying on Spring's automatic serialization.

java
@GetMapping("/secure/{id}")
public ResponseEntity<Product> secureCall(@PathVariable Long id) {
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_JSON);
    headers.set("Authorization", "Bearer " + fetchToken());

    HttpEntity<Void> requestEntity = new HttpEntity<>(headers);

    ResponseEntity<Product> response = restTemplate.exchange(
        "http://localhost:8082/product/" + id,
        HttpMethod.GET,
        requestEntity,
        Product.class
    );
    return response;
}

exchange returns ResponseEntity<T>, giving access to the status code, headers, and body.


The execute Method — Complete Manual Control

execute is the lowest level of RestTemplate. All other methods internally delegate to it. Use it only when you need full control over both request serialization and response deserialization.

java
restTemplate.execute(
    "http://localhost:8082/product/1",
    HttpMethod.GET,
    request -> {
        // RequestCallback: modify the ClientHttpRequest before it is sent
        request.getHeaders().set("X-Custom-Header", "value");
    },
    response -> {
        // ResponseExtractor: read the ClientHttpResponse manually
        return StreamUtils.copyToString(response.getBody(), StandardCharsets.UTF_8);
    }
);

Limitations of RestTemplate

RestTemplate is in maintenance mode — Spring will not add new features, only critical bug fixes. The core problems:

  1. Explosion of overloaded methodsgetForObject, getForEntity, postForObject, postForEntity, exchange × 3 overloads each, execute × 2 overloads. Difficult to remember and maintain.
  2. Designed before modern HTTP features — adding support for interceptors, circuit breakers, or retry requires yet more overloads for each existing variant.
  3. Not fluent — building a request requires knowing which specific overload to call for each combination of method, headers, and body.

These limitations led to RestClient (Spring 6+) and FeignClient (Spring Cloud), covered in the following chapters.


Interview Questions & Pitfalls

Q1. What is the difference between getForObject and getForEntity?

getForObject returns only the deserialized response body. getForEntity returns a ResponseEntity wrapper containing the body, HTTP status code, and response headers. Use getForEntity whenever you need to inspect the status code or headers of the response.

Q2. What does connectTimeout mean vs. readTimeout in RestTemplate?

connectTimeout is the maximum time the client waits while establishing the underlying TCP connection (the three-way handshake). readTimeout is the maximum time the client waits for the server to send a response after the connection is open. A timeout on one does not affect the other.

Q3. RestTemplate is described as "legacy." Can it still be used in Spring Boot 3?

Yes, it is still fully functional in Spring Boot 3. It is in maintenance mode, meaning no new features will be added, but existing behavior will be preserved with bug fixes. For greenfield projects, prefer RestClient or FeignClient.

Q4. What is the KeepAliveCache and why should you care about it?

In HTTP 1.1, connections are kept alive by default. RestTemplate (via HttpUrlConnection) maintains a KeepAliveCache that maps (host, port) to live HTTP client objects. Reusing these avoids the overhead of a new TCP handshake for each request. When you call disconnect() on a fully read response, the connection is not actually closed — it is returned to the cache. Understanding this is important when diagnosing connection pool exhaustion.

Q5. What happens internally when you call restTemplate.getForObject(url, Product.class)?

Spring calls SimpleClientHttpRequestFactory.createRequest() to build an HttpUrlConnection, sets method and headers, calls connection.connect() (which checks the KeepAliveCache before opening a new TCP connection), calls getResponseCode() to send the request and receive the response, passes the response stream through an HttpMessageConverter to deserialize it into Product, closes the stream, and returns the deserialized object. The TCP connection is not closed but is returned to the keep alive cache.

Q6. When should you use exchange instead of getForEntity or postForEntity?

Use exchange when you need to customize the HTTP method or request headers independently of what RestTemplate's named methods assume. For example, sending a GET request with a body or a DELETE with custom headers requires exchange because the named methods do not expose those combinations.

Q7. What is a common pitfall when hardcoding service URLs in RestTemplate?

Hardcoded URLs couple the calling service to a specific host and port. This breaks if the target service scales to multiple instances, moves to a different host, or changes ports. The production pattern is to use service discovery (Eureka) together with a load balancer so that only the service name appears in the URL (e.g., http://product-service/product/1), and the framework resolves it dynamically.