I’ve been building microservices for years, and one thing I keep running into is the mess that comes with calling other APIs. You write the same boilerplate again and again: building a RestTemplate call, parsing the response, handling errors, threading headers, and then fixing the inevitable typo in a URL string that only shows up at runtime. It drove me crazy. Then I remembered Retrofit. I used it back in my Android days, but I never considered it for server-side Java. That was a mistake. Retrofit, backed by OkHttp, gives you compile-time validation of your API contracts. You define an interface with annotations, and the library generates the implementation. No more stringly-typed URLs. No more forgetting to pass the auth token. And when you combine it with Spring Boot, you get a fully managed, production-ready HTTP client that’s testable and resilient.
But why now? Because Spring officially deprecated RestTemplate and while WebClient is fine, it forces you into reactive programming even if you don’t need it. OpenFeign works well but ties you to Spring Cloud. Retrofit is framework‑agnostic, lightweight, and works seamlessly with OkHttp’s interceptor chain. It’s the Swiss army knife of HTTP clients, and I think more people should use it on the JVM backend.
Let me show you how I set it up in a Spring Boot 3 application. First, you need the right dependencies. I added Retrofit, the Jackson converter (so it uses Spring’s ObjectMapper), OkHttp, and the logging interceptor. I also include Resilience4j later for circuit breakers. Here’s a snippet from my pom.xml:
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>retrofit</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>converter-jackson</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>logging-interceptor</artifactId>
<version>4.12.0</version>
</dependency>
Now the core: defining the API interface. This is where the compile‑time validation kicks in. I write an interface like this:
public interface UserApiClient {
@GET("users/{id}")
Call<UserDto> getUser(@Path("id") Long id);
@POST("users")
Call<UserDto> createUser(@Body CreateUserRequest request);
}
Notice the @Path, @Body, @GET, @POST. If I misspell @GET or use a wrong parameter name, the compiler catches it. That’s huge. No more runtime 404s from a missing slash in your URL.
Now I need to create a Spring bean that configures the Retrofit instance. I usually put this logic in a @Configuration class. I want to control timeouts, connection pooling, and most importantly, I want to inject authentication headers and propagate trace IDs. OkHttp interceptors are perfect for that. Here’s how I build the OkHttpClient:
@Bean
public OkHttpClient okHttpClient() {
return new OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.connectionPool(new ConnectionPool(10, 5, TimeUnit.MINUTES))
.addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
.addInterceptor(chain -> {
Request original = chain.request();
Request request = original.newBuilder()
.header("Authorization", "Bearer " + tokenProvider.getToken())
.header("X-Request-Id", MDC.get("traceId"))
.method(original.method(), original.body())
.build();
return chain.proceed(request);
})
.build();
}
That interceptor runs on every request. It adds the Bearer token and a trace ID from the MDC context. No manual header threading in the service layer. It’s clean.
Then I wire it into Retrofit:
@Bean
public UserApiClient userApiClient(OkHttpClient client, ObjectMapper mapper) {
return new Retrofit.Builder()
.baseUrl("https://user-service.internal.example.com/api/v1/")
.client(client)
.addConverterFactory(JacksonConverterFactory.create(mapper))
.build()
.create(UserApiClient.class);
}
Now, wherever I inject UserApiClient in my services, I can call methods like any other Java interface. But we need to handle errors uniformly. Retrofit’s Call returns either a successful response or an error. I don’t want to check response.isSuccessful() everywhere. I created a custom CallAdapter that wraps everything into a Result<T> type – either Result.Success or Result.Failure. Then I can process it in one place, mapping HTTP errors to domain exceptions.
Have you ever had to deal with a 503 from a downstream service and then have the whole request fail? That’s where resilience comes in. I integrate Resilience4j with Retrofit using the resilience4j-retrofit module. I define a RetryConfig with exponential backoff and a CircuitBreakerConfig. Then I wrap the Retrofit call in a DecoratedCallFactory. Now my client is bulletproof: if the user service returns a 503, it retries up to three times with increasing delays, and if too many failures occur, the circuit breaker opens and we fall back to a cached response.
Here’s a simplified version of how I configure the retry:
RetryConfig config = RetryConfig.custom()
.maxAttempts(3)
.waitDuration(Duration.ofMillis(500))
.retryOnResult(response -> response.isSuccessful() == false && response.code() >= 500)
.build();
RetryRegistry registry = RetryRegistry.of(config);
Retry retry = registry.retry("userService");
Call<UserDto> decoratedCall = Retry.decorateCall(retry, () -> userApiClient.getUser(42L));
But wait – there’s more. Testing this whole setup is easy thanks to OkHttp’s MockWebServer. I spin up a mock server in my JUnit test, set expectations, and verify that my client sends the right headers, handles timeouts, and executes retry logic correctly. For integration tests, I use Testcontainers with a real service stub. That’s the level of confidence I need.
You might ask: How does Retrofit compare to the new Spring RestClient? RestClient is synchronous and fluent, but it still doesn’t give you compile‑time contract validation. You’re still building the request path as a string. Retrofit forces you to declare the contract upfront. If you change the API path, the compiler tells you. That’s worth its weight in gold.
I’ve been using this setup for six months now. The code is cleaner, my error rates dropped because of the uniform handling, and new developers on the team can read the API interfaces and immediately understand which endpoints we call. No more digging through RestTemplate invocations buried in service methods.
If you’ve struggled with REST client spaghetti, give Retrofit a try. It’s not just for Android anymore. It’s a serious tool for JVM backend services.
I hope this helps you build more robust integrations. If you found it useful, please like, share, and comment below – I’d love to hear how you handle HTTP clients in your projects.
As a best-selling author, I invite you to explore my books on Amazon. Don’t forget to follow me on Medium and show your support. Thank you! Your support means the world!
101 Books
101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.
Check out our book Golang Clean Code available on Amazon.
Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!
📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!
Our Creations
Be sure to check out our creations:
Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools
We are on Medium
Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva