OkHttp Fundamentals
Before you can call Black Book, Carfax, or S&P VIS safely, you need an HTTP client that reuses connections, times out predictably, and lets you hook into every request.
VehicleValuationService is about to make three kinds of outbound calls it does not fully control: requests to Black Book, Carfax, and S&P VIS, each a separate company with its own uptime, its own latency profile, and its own bad day. Before you write a single line of provider-specific code, you need a solid, shared foundation for making HTTP calls from the JVM — and on the JVM, that foundation is almost always OkHttp. It is the library Retrofit builds on, the library most Kotlin HTTP tooling assumes you are using underneath, and it is fast enough and low-level enough that you can reason precisely about what happens on the wire.
The first decision, and one that is easy to get wrong, is how many `OkHttpClient` instances to create. An `OkHttpClient` is not a lightweight request builder — constructing one spins up a connection pool, a dispatcher with its own thread pool, and caches for things like DNS results. Creating a new one for every call to Black Book throws all of that away after a single request, forcing OkHttp to open a fresh TCP connection and — since these are HTTPS endpoints — renegotiate a fresh TLS handshake every single time. The fix is to build exactly one `OkHttpClient` for the whole application, usually as a Spring bean, and inject it into every provider client. Its `ConnectionPool` then keeps recently-used keep-alive connections open, so the second call to the same host reuses an already-warm connection instead of paying connection setup costs again.
Timeouts are the next thing to get right, and OkHttp gives you four separate knobs: `connectTimeout` (how long to wait for the TCP handshake), `readTimeout` (how long to wait between bytes once the response starts streaming), `writeTimeout` (the same, for the request body), and `callTimeout` (a hard ceiling on the entire call, connect through response, that overrides the others). Leaving these at OkHttp's defaults — ten seconds each, with no overall call timeout — is dangerous for a service like this one: if Black Book is having a bad day and every socket read stalls for nine seconds before failing, a caller hitting `VehicleController` for a valuation is going to wait far longer than any reasonable API contract should allow. Set aggressive, explicit timeouts (a couple of seconds for connect, a few for read) and always set `callTimeout` as a backstop, so one misbehaving provider can never make its slowness someone else's problem.
Building an actual request is the straightforward part: `Request.Builder()` takes a URL, a method, headers, and an optional body, and produces an immutable `Request` you hand to `client.newCall(request)`. The part that trips people up is the `Response` you get back — it holds an open connection and a streaming body, and if you do not close it, that connection never gets returned to the pool. The fix is almost always `response.use { ... }`, Kotlin's version of try-with-resources, which closes the response (and releases the connection) whether your parsing code succeeds or throws. Forgetting this does not fail loudly; it just slowly exhausts the connection pool under load, which is exactly the kind of bug that only shows up once you are under real traffic.
OkHttp gives you two ways to actually run a call. `client.newCall(request).execute()` runs synchronously and blocks the calling thread until a response (or exception) comes back — simple, and the shape you will reach for from inside a Kotlin coroutine. `client.newCall(request).enqueue(callback)` is asynchronous: it returns immediately and invokes your `Callback`'s `onResponse` or `onFailure` on one of OkHttp's own dispatcher threads later. Inside a `suspend fun`, you generally do not need `enqueue` at all — you can call the blocking `execute()` from inside `withContext(Dispatchers.IO)` and get the same non-blocking behavior from the coroutine's point of view, without writing a single callback. That pattern is exactly what the next lesson's provider clients use.
The last piece, and the one that pays for itself the moment you have three providers instead of one, is the `Interceptor`. An interceptor sits in the request/response pipeline and can inspect, modify, retry, or short-circuit anything passing through — chain multiple together and each one wraps the next. Two interceptors earn their place in almost every real service: one that stamps a shared API-key header onto every outbound request (so no individual client has to remember to add it, and rotating the key means changing one place), and `HttpLoggingInterceptor`, which logs method, URL, status, and timing for every call OkHttp makes. Wire the logging interceptor's output into the same pipeline that feeds Datadog, and you get outbound-call visibility for free, before you have written a single provider-specific line of code.
None of this is provider-specific yet, and that is the point: everything in this lesson — the shared client, its timeouts, its connection pool, its interceptors — is infrastructure that `BlackBookClient`, `CarfaxClient`, and `VisClient` will all sit on top of in the next lesson. Get this foundation right once, and every provider client you write afterward is a thin, provider-specific layer instead of three parallel reimplementations of connection pooling and timeout handling.
import okhttp3.ConnectionPoolimport okhttp3.OkHttpClientimport org.springframework.context.annotation.Beanimport org.springframework.context.annotation.Configurationimport java.util.concurrent.TimeUnit@Configurationclass HttpClientConfig {// One OkHttpClient for the whole application. Building a new client per// request would spin up a fresh connection pool and dispatcher thread// pool every time -- expensive, and it throws away the keep-alive// connections that make repeated calls to Black Book, Carfax, and VIS fast.@Beanfun sharedOkHttpClient(): OkHttpClient =OkHttpClient.Builder().connectTimeout(2, TimeUnit.SECONDS).readTimeout(3, TimeUnit.SECONDS).writeTimeout(3, TimeUnit.SECONDS).callTimeout(5, TimeUnit.SECONDS) // hard ceiling on the whole call.connectionPool(ConnectionPool(20, 5, TimeUnit.MINUTES)).build()}
Configures one shared, connection-pooled OkHttpClient as a Spring bean with explicit timeouts; it can't run in-browser because it needs a real JVM, Spring's ApplicationContext, and TCP/TLS sockets, none of which exist in the sandbox.
import okhttp3.Interceptorimport okhttp3.OkHttpClientimport okhttp3.Responseimport okhttp3.logging.HttpLoggingInterceptorclass ApiKeyInterceptor(private val apiKey: String) : Interceptor {override fun intercept(chain: Interceptor.Chain): Response {val authenticated = chain.request().newBuilder().header("Authorization", "Bearer $apiKey").build()return chain.proceed(authenticated)}}// Attach interceptors when the client is built. Every request that flows// through this client -- to any provider -- picks up the header and gets// logged, without every ValuationProviderClient having to remember to do it.fun buildInstrumentedClient(base: OkHttpClient.Builder, apiKey: String): OkHttpClient {val logging = HttpLoggingInterceptor().apply {level = HttpLoggingInterceptor.Level.BASIC}return base.addInterceptor(ApiKeyInterceptor(apiKey)).addInterceptor(logging).build()}
Adds a shared API-key header and request logging via interceptors so every outbound call, regardless of which provider client makes it, is authenticated and observed the same way; this needs OkHttp's real Interceptor chain and network I/O, so it can't execute in-browser.
import okhttp3.OkHttpClientimport okhttp3.Request// Synchronous execution with safe resource handling. The use { } block// guarantees the response body underlying connection is released back to// the pool even if parsing throws -- forgetting this is the single most// common OkHttp leak, and it slowly starves the connection pool under load.fun fetchRaw(client: OkHttpClient, url: String): String {val request = Request.Builder().url(url).get().build()client.newCall(request).execute().use { response ->if (!response.isSuccessful) {throw IllegalStateException("Unexpected code $response")}return response.body?.string().orEmpty()}}
Executes a request synchronously and safely closes the response with use { } to release the connection back to the pool; it can't run in-browser because it opens a real socket to a real server.
🧠 Check your understanding
0/1 · 0/1 answered1. The valuation service builds exactly one OkHttpClient and injects it into BlackBookClient, CarfaxClient, and VisClient, rather than letting each client construct its own. What is the main reason for sharing a single instance?