Engineering Production-Grade API Clients in Spring Boot
Engineering Production-Grade API Clients in Spring Boot
> Part 1: Seeting up the API CLient
ximanta.sarma@gmail.com
3 min readMar 2026
Most backend systems eventually need to integrate with external APIs, such as Payment gateways. Tax validation services. Identity providers. CRM systems, and more.
At first, integrating with an API seems simple. You use the old familiar RestTemplate.
And it works fine. Until a few weeks later, when the requirements start growing. You suddenly need authentication tokens, retries, error handling, logging, request models, and testing
Now HTTP calls are scattered across services and controllers, and debugging production issues becomes painful. The solution is simple but often overlooked:
Treat external APIs as proper clients — not just HTTP calls.
This article is Part 1 of the series that walks through a clean architecture pattern for building production-grade API clients in Spring Boot.
The Problem with Direct HTTP Calls
A common anti-pattern looks like this:
Controller → Service → Direct HTTP call
This approach leads to:
• Duplicated HTTP logic
• Scattered authentication code
• Inconsistent error handling
• Poor testability
Instead, external APIs should live behind dedicated client layers.
The Architecture Pattern
A better architecture looks like this:
Application Layer → External API Client (Service Layer) → HTTP Client (RestClient) → External API
Or visually:
Related Articles
Shared topics and tags
More articles in this cluster will appear here as additional posts share the same topics or tags.
Newsletter
Expert notes in your inbox
Subscribe for new articles.
Controller → Business Service → External API Client → RestClient → External Service
This gives you clean boundaries, reusable integrations, centralized configuration, and easy testing.
Step 1 — Create a Dedicated Client Module
In larger systems, external integrations should live in separate modules in a Spring Boot multi-module project.
Spring Boot 3 introduced RestClient, a modern replacement for RestTemplate.
A simple configuration:
@Configuration
public class TaxApiClientConfig {
@Bean
public RestClient taxApiRestClient(
RestClient.Builder builder,
TaxApiProperties properties) {
return builder
.baseUrl(properties.getApiUrl())
.defaultHeader("Content-Type", "application/json")
.build();
}
}
Now all API calls share the same configuration.
Step 4 — Create a Client Service
Instead of calling RestClient everywhere, expose a dedicated client service.
public interface TaxValidationService {
ValidationResponse validateTaxId(ValidationRequest request);
}
An implementation can be this:
@Service
public class TaxValidationServiceImpl implements TaxValidationService {
private final RestClient restClient;
public TaxValidationServiceImpl(RestClient restClient) {
this.restClient = restClient;
}
@Override
public ValidationResponse validateTaxId(ValidationRequest request) {
ResponseEntity<ValidationResponse> response =
restClient.post()
.uri("/validate")
.body(request)
.retrieve()
.toEntity(ValidationResponse.class);
return response.getBody();
}
}
Your application code now depends on clean abstractions, not HTTP calls.
Step 5 — Centralize Error Handling
External APIs fail in unpredictable ways, like invalid requests, expired tokens, rate limits, server errors, and others.
A common pattern is to implement a custom error handler.
public class ExternalApiErrorHandler
implements RestClientResponseErrorHandler {
@Override
public void handleError(ClientHttpResponse response) {
throw new ExternalApiException(
"External API error: " + response.getStatusCode());
}
}
Avoid raw JSON maps. Instead define request and response models.
public class ValidationRequest {
private String taxId;
private String name;
}
public class ValidationResponse {
private String status;
private String details;
}
You get the benefits of compile-time safety, better documentation, and easier debugging.
Step 7 — Test Without Calling the Real API
Production systems should never rely on real APIs during unit tests. Instead, mock the HTTP client.
@ExtendWith(MockitoExtension.class)
class TaxValidationServiceTest {
@Mock
private RestClient restClient;
}
This keeps tests: fast, deterministic, and independent of network failures.
Step 8 — Add Request Logging
Debugging API integrations can be difficult. Libraries like Logbook provide structured request and response logging. This helps diagnose issues like malformed requests, authentication failures, unexpected responses, among others.
Production API Client Checklist
A production-ready API client should, at a minimum, provide configuration properties, centralized HTTP configuration, dedicated client service, typed request/response models, consistent error handling, logging, and unit tests.
If any of these are missing, your integration will eventually become difficult to maintain.
Final Thoughts
External APIs are critical parts of modern systems, but they are often treated like simple HTTP calls. By designing integrations as well-structured clients, you gain cleaner architecture, better reliability, easier testing, and maintainable code.
In large systems, this small architectural decision can prevent a huge amount of technical debt.
The key takeaway: Treat external APIs like first-class infrastructure components, not quick integrations.
In the next part, I will be sharing the best practices for OAuth Token Caching in a Spring Boot API Client.