RestTemplate - API Communication

👉 Master synchronous HTTP calls, timeouts, interceptors, error handling and best practices in Spring Boot.


What is Spring’s RestTemplate ?

Spring RestTemplate is a synchronous HTTP client used to perform RESTful requests and simplify communication between microservices.

It abstracts the complexity of low-level HTTP client libraries by providing a template-method API that handles request creation, execution, and response mapping to Java objects.

In a microservice architecture, one service often needs to fetch data from another. RestTemplate acts as the bridge, allowing the “Client” service to consume resources from a “Provider” service over HTTP.


Why use RestTemplate ?

RestTemplate exists to standardize how Spring applications communicate with external REST APIs. It handles boilerplate tasks like opening connections, managing headers, and converting JSON/XML payloads into POJOs.

  • Simplified API:
    • Offers high-level methods like getForEntity, postForObject, and exchange.
  • Automatic Serialization:
    • Integration with Jackson ensures JSON responses are mapped to Java beans automatically.
  • Synchronous Execution:
    • The calling thread blocks until the response is received, ensuring a straightforward sequential flow.
  • Customizable:
    • Supports interceptors, error handlers, and message converters for production-grade requirements.

Application configuration

To use RestTemplate in Spring Cloud environment, application must have the Spring Web starter dependency.

This dependency provides the necessary infrastructure for both hosting as well as consuming RESTful services.

Maven dependency

Ensure below dependency is present in application’s pom.xml, add if missing.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

RestTemplate configuration

In production environments, using the default RestTemplate constructor is discouraged because it lacks timeout controls.

Instead, use a RestTemplateBuilder to configure a shared, thread-safe Bean, autowired by spring in service class.

Define a configuration class to manage the lifecycle and settings of the client.

@Configuration
public class RestTemplateConfiguration {

    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder builder) {
        return builder
                .setConnectTimeout(Duration.ofMillis(3000)) // Time to establish connection
                .setReadTimeout(Duration.ofMillis(5000))    // Time to wait for data
                .build();
    }
}


Consume Rest services

Communicating between services requires a Response Bean to hold the data and a Client to initiate the request.

Below example demonstrates how a to build RestTemplate and consume Rest services.

1. The Data Model (POJO)

The response bean must have fields that match the JSON structure returned by the provider service.

public class CurrencyConversion {
    private Long id;
    private String from;
    private String to;
    private BigDecimal conversionMultiples;
    private BigDecimal quantity;
    private BigDecimal totalCalculatedAmount;
    private String environment;

    // Default constructors, getters, and setters
}

2. The service API integration

Inject the RestTemplate bean and use getForEntity to perform a GET request with URI variables.

@RestController
public class CurrencyConversionController {

    // Update this to your app base url.
    private static final String BASE_URL = "http://localhost:8000";
    private static final String EXCHANGE_API_PATH = 
                            "/currency-exchange/from/{from}/to/{to}";
    
    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/currency-conversion/from/{from}/to/{to}/q/{quantity}")
    public CurrencyConversion calculateConversion(
        @PathVariable String from, 
        @PathVariable String to, 
        @PathVariable BigDecimal quantity) {

        // 1. Prepare URI Variables
        Map<String, String> uriVariables = new HashMap<>();
        uriVariables.put("from", from.toUpperCase());
        uriVariables.put("to", to.toUpperCase());

        // 2. Execute Request
        ResponseEntity<CurrencyConversion> response = 
            restTemplate.getForEntity(
                BASE_URL + EXCHANGE_API_PATH, 
                CurrencyConversion.class, 
                uriVariables);

        // 3. Null Safety Check (Crucial for Beginners)
        CurrencyConversion result = response.getBody();
        if (result == null) {
            throw new RuntimeException(
              "Data not found for the requested currency pair.");
        }

        // 4. Perform Calculation
        result.setQuantity(quantity);
        BigDecimal multiple = result.getConversionMultiples();
        
        if (multiple != null) {
            result.setTotalCalculatedAmount(
                    quantity.multiply(multiple));
        }

        return result;
    }
}

RestTemplate lifecycle

The RestTemplate Request-Response Lifecycle describes the internal flow of an HTTP call, starting from the application code through the underlying HTTP client to the external service.

This sequence ensures that every request is intercepted, executed, and converted back into a usable Java format.

sequenceDiagram
    participant App as Client Application
    participant RT as RestTemplate
    participant Int as Interceptors
    participant Net as External Microservice
    participant Conv as MessageConverter

    App->>RT: call getForEntity() / exchange()
    RT->>Int: processInterceptors (Auth, Logging)
    Int->>Net: Send HTTP Request
    Note right of Net: Provider processes logic
    Net-->>Int: Return HTTP Response
    Int-->>RT: Pass Response
    RT->>RT: execute ErrorHandler
    RT->>Conv: convert Response Body to POJO
    Conv-->>RT: Java Object
    RT-->>App: ResponseEntity<T>

Core components in the lifecycle

Several key components collaborate during this lifecycle to ensure reliable communication between the microservices. Each component has a specific responsibility in the transformation of data.

ComponentPurpose
ClientHttpRequestFactoryThe engine that creates the actual HTTP connection using libraries like Apache HttpClient or OkHttp.
HttpMessageConvertersResponsible for marshaling and unmarshaling the data, such as converting a Java object to JSON for a POST request.
ResponseErrorHandlerA strategy interface used to determine whether a particular HTTP response has an error and how to handle it.
ClientHttpRequestInterceptorA mechanism to intercept outgoing requests to add headers, such as Bearer Tokens or Correlation IDs for distributed tracing.

In-Built default strategies

The table below summarizes how RestTemplate manages different parts of the HTTP lifecycle to maintain flexibility.

ComponentResponsibilityDefault Implementation
Request FactoryConnection ManagementSimpleClientHttpRequestFactory
ConvertersData TransformationMappingJackson2HttpMessageConverter
Error HandlingStatus Code ValidationDefaultResponseErrorHandler

Client request interceptors

A ClientHttpRequestInterceptor is a functional interface in Spring that allows you to intercept outgoing HTTP requests and incoming responses.

It acts as a middleware for RestTemplate, enabling developers to globally modify headers, inject authentication tokens, or log request/response metadata across all microservice interactions.

In a distributed system, interceptors are critical for maintaining observability. Without them, debugging failures between services becomes difficult, as there is no central record of what was sent or received.


Why to use interceptors ?

Interceptors solve the problem of repetitive code by centralizing cross-cutting concerns for HTTP communication.

  • Distributed Tracing:
    • Injecting X-B3-TraceId or correlation IDs to track a request across multiple services.
  • Security:
    • Automatically attaching JWT or Bearer tokens to every outgoing call.
  • Audit Logging:
    • Recording the URL, method, and execution time of every external request for compliance.
  • Performance Monitoring:
    • Measuring how long downstream services take to respond.

Implementation: Logging Interceptor

To log the request and response body, we have to implement the ClientHttpRequestInterceptor interface.

Note: To log the response body without consuming the stream (which would prevent the application from reading it), we must use a specific ClientHttpRequestFactory.

1. Implement the interceptor

This class captures the request details before execution and logs the response status after the call returns.

public class RestTemplateLoggingInterceptor 
                        implements ClientHttpRequestInterceptor {

    private static final Logger log = LoggerFactory
                        .getLogger(RestTemplateLoggingInterceptor.class);

    @Override
    public ClientResponse intercept(HttpRequest request,
                        byte[] body, 
                        ClientHttpRequestExecution execution) 
                        throws IOException {
        logRequest(request, body);
        ClientHttpResponse response = execution.execute(request, body);
        logResponse(response);
        return response;
    }

    private void logRequest(HttpRequest request, byte[] body) {
        log.info("Request: {} {}", request.getMethod(), request.getURI());
        log.debug("Request Body: {}", 
                        new String(body, StandardCharsets.UTF_8));
    }

    private void logResponse(ClientHttpResponse response) 
                                            throws IOException {
        log.info("Response Status: {}", response.getStatusCode());
    }
}

2. Register the interceptor

When defining your RestTemplate bean, we must register the interceptor and wrap the factory in a BufferingClientHttpRequestFactory. So, the response stream can be read twice (once for logging, another by the service).

@Configuration
public class RestTemplateConfiguration {

    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder builder) {

        RestTemplate restTemplate = builder
                .setConnectTimeout(Duration.ofMillis(3000))
                .setReadTimeout(Duration.ofMillis(5000))
                .build();
        
        // Enables buffering, so response body can be read multiple times
        restTemplate.setRequestFactory(
            new BufferingClientHttpRequestFactory(
                new SimpleClientHttpRequestFactory()));

        List<ClientHttpRequestInterceptor> interceptors =
                                    restTemplate.getInterceptors();
        if (CollectionUtils.isEmpty(interceptors)) {
            interceptors = new ArrayList<>();
        }
        interceptors.add(new RestTemplateLoggingInterceptor());
        restTemplate.setInterceptors(interceptors);

        return restTemplate;
    }
}

Caution:
→ Use the BufferingClientHttpRequestFactory with caution, If the downstream service returns a large payload (e.g., a 10MB JSON), it will load the entire payload into memory, which can lead to OutOfMemoryError under high load.


Benefits of interceptor strategy

Using interceptors ensures your microservice architecture remains clean and maintainable.

FeatureWithout InterceptorsWith Interceptors
Code DuplicationHigh (Log in every controller)Zero (Centralized)
MaintenanceDifficult (Update every call)Easy (Update one class)
TraceabilityManual / FragmentedAutomatic / Consistent

Handling communication failures

In production-grade microservices, Communication Failures occur when a downstream service is unavailable, slow, or returns error status codes.

RestTemplate manages these failures through a specialized ResponseErrorHandler interface, which prevents the application from crashing due to unhandled 4xx or 5xx exceptions.

Handling these failures correctly is vital for system resilience. If a “Client” service does not handle a “Provider” service failure gracefully, it can lead to a cascading failure that brings down the entire distributed system.


1. Custom ResponseErrorHandler

The ResponseErrorHandler is a strategy interface used to determine if a response has an error and how to process it before it reaches your business logic.

In production grade apps, this is used to map obscure HTTP errors into meaningful domain-specific exceptions.

RestTemplateErrorHandler impl

Below error handler, identifies the error responses and do the needful to process the error gracefully.

@Component
public class CustomRestTemplateErrorHandler 
                    implements ResponseErrorHandler {

    @Override
    public boolean hasError(ClientHttpResponse response) 
                    throws IOException {
        return response.getStatusCode().isError();
    }

    @Override
    public void handleError(ClientHttpResponse response)
                    throws IOException {
        if (response.getStatusCode().is4xxClientError()) {
            // Logic for 4xx: Log and throw custom ClientException
            throw new ResourceNotFoundException(
                "Downstream service resource not found.");
        } else if (response.getStatusCode().is5xxServerError()) {
            // Logic for 5xx: Log and throw custom ServerException
            throw new ServiceUnavailableException(
                "Downstream service is currently unavailable.");
        }
    }
}

2. Production resilience patterns

While a ResponseErrorHandler handles the logic of an error, production-grade apps use Resilience Patterns to manage the behavior of the connection itself.

We will be discuss in details on these topics in upcoming sessions.

Insight:
→ Relying solely on error handling is insufficient for high-availability systems.

Key resilience strategies

  • Circuit Breaker:
    • Prevents the application from repeatedly calling a failing service, allowing it time to recover.
  • Retry Pattern:
    • Automatically re-attempts a failed request a specific number of times (useful for transient network glitches).
  • Bulkhead:
    • Limits the number of concurrent calls to a specific service to prevent resource exhaustion.
  • Timeouts:
    • Ensures a slow service does not hang the calling thread indefinitely (as discussed in configuration).

3. Error handling strategies

Choosing the right strategy depends on the criticality of the downstream data.

StrategyBest ForImplementation
ResponseErrorHandlerMapping HTTP status to ExceptionsCustom Java Class
Try-Catch BlocksOne-off, specific logic callsInline Controller code
Resilience4jPreventing cascading failures@CircuitBreaker Annotation
Fallback MethodsProviding default data when service failsSecondary “Default” method

4. Best Practices

To ensure that the microservice remains robust, follow these architectural principles for communication.

  • Never Use Defaults:
    • Always configure explicit connect and read timeouts.
  • Log Metadata, Not Data:
    • Log the request URL and status code.
    • Avoid logging sensitive payload data.
  • Use Fallbacks:
    • If a service fails return either of below instead of an error page.
      • A cached value
      • A meaningful default response
  • Propagate Traces:
    • Ensure your interceptors pass correlation IDs to track the error across service boundaries.

Other communication clients

Spring provides multiple ways to handle service-to-service communication depending on your project requirements.

FeatureStyleBoilerplateStatus
RestTemplateSynchronous, Template-basedHigh (needs URL, Map, etc.)Maintenance Mode
RestClient(Spring 6.1+)Synchronous, Fluent APIMedium (Chainable methods)Active / Recommended
OpenFeignDeclarative, Interface-basedVery Low (Annotations only)Active (Spring Cloud)

Architect's Note:
→ While RestTemplate is the industry standard for older projects.
OpenFeign is preferred in Spring Cloud for its cleaner, interface-driven approach.


References:
→ Spring documentation on RestTemplate.