Modeling Third-Party Clients: Black Book, Carfax, and S&P VIS
One shared contract, three provider-specific implementations, each isolated enough that a Carfax quirk can never break a Black Book call.
With a shared, well-configured OkHttpClient in hand, the next question is what VehicleValuationService actually depends on when it needs a valuation. It should not depend on OkHttp directly, and it should not depend on `BlackBookClient` or `CarfaxClient` by name — it should depend on an abstraction that hides which provider it is talking to. That abstraction is `ValuationProviderClient`, a small interface with one method: `suspend fun fetchValuation(vin: String): ValuationResult`. Every provider implements it the same way, which means the service can hold a `List<ValuationProviderClient>`, call all three concurrently, and never care which one is Black Book and which one is Carfax.
Notice that the interface is `suspend`. Nothing about fetching a valuation is CPU-bound — it is almost entirely waiting on a network round trip — so this is exactly the kind of work coroutines exist for. A suspend function lets `VehicleValuationService` launch three of these calls concurrently with `async`, await all three, and feed the results into the rules engine, without ever blocking a thread for the ten-or-so seconds all three providers combined might take if called one after another.
The implementation is one class per provider: `BlackBookClient`, `CarfaxClient`, `VisClient`. Each one owns exactly one provider's base URL, exactly one provider's auth scheme, and exactly one provider's response shape — parsing that JSON into the same domain type, `ValuationResult`, that the rest of the service understands. From the outside, all three look identical; from the inside, each is free to be as weird as its provider actually is.
It is tempting, especially early on, to build one `GenericValuationClient` that takes a `provider: String` and branches internally with a `when` on parsing logic, headers, and URLs. Resist it. The moment Carfax changes its response schema or starts rate-limiting more aggressively, you are editing a class that also handles Black Book and VIS, and a typo in the Carfax branch can take down a code path that has nothing to do with Carfax. Modeling each provider as its own class means a bug, an outage, or a breaking API change in one provider has a blast radius of exactly one class — it cannot leak into how you talk to the other two, and you can unit test, redeploy, or even temporarily disable one client without touching the others.
The providers really do differ in exactly the ways that make this worth doing. Black Book returns a flat JSON object with the number you want one level deep; Carfax nests it three levels down inside a `history` object and reports whole dollars where Black Book reports a decimal; a real S&P VIS integration might report cents directly, or wrap errors in a 200-status envelope instead of using proper HTTP status codes. None of that idiosyncrasy is a design flaw in your service — it is just what integrating with three independent companies looks like — and keeping each quirk contained inside its own client is what stops it from becoming your problem everywhere else.
Inside each client, the actual OkHttp call is still a blocking one — `execute()` parks the calling thread until bytes come back. Wrapping it in `withContext(Dispatchers.IO)` is what makes it coroutine-friendly: `Dispatchers.IO` is a large, elastic thread pool built specifically to absorb blocking work like this, so the blocking call happens there instead of on whatever dispatcher called `fetchValuation` in the first place. Skipping that `withContext` call and just invoking the blocking OkHttp code directly from a suspend function would silently block whatever thread happens to be running that coroutine — which, on `Dispatchers.Default`, means stealing one of a small, fixed pool of threads meant for CPU work, exactly when the rules engine needs them.
With three provider clients built to the same interface and each responsible only for its own quirks, the service has a clean seam to build resilience against. Neither Black Book, Carfax, nor VIS is going to be reliable one hundred percent of the time — that is precisely the premise of this module — and the next two lessons add retries and circuit breakers as behavior that wraps around any `ValuationProviderClient`, without any of the three implementations needing to know it is there.
import java.time.Instant// The one contract the rest of the service depends on. It knows nothing// about HTTP, JSON shapes, or provider-specific auth -- just "give me a// valuation for this VIN, suspended until it is ready or it fails."interface ValuationProviderClient {val providerName: Stringsuspend fun fetchValuation(vin: String): ValuationResult}data class ValuationResult(val provider: String,val vin: String,val estimatedValueCents: Long,val confidence: Double,val retrievedAt: Instant)class ValuationProviderException(provider: String, statusCode: Int) :RuntimeException("$provider returned HTTP $statusCode")
Defines the shared contract every provider client implements, plus the domain model they all produce; it can't run in-browser because it is meant to be compiled as part of the Spring Boot application alongside the real client implementations, not executed standalone.
import com.fasterxml.jackson.databind.JsonNodeimport com.fasterxml.jackson.databind.ObjectMapperimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.withContextimport okhttp3.OkHttpClientimport okhttp3.Requestimport java.time.Instantclass BlackBookClient(private val client: OkHttpClient,private val mapper: ObjectMapper,private val baseUrl: String) : ValuationProviderClient {override val providerName = "black-book"override suspend fun fetchValuation(vin: String): ValuationResult =// OkHttp execute() is a blocking call. Dispatchers.IO is a large,// elastic thread pool meant exactly for blocking I/O like this --// running it there instead of on the calling dispatcher keeps us// from starving the coroutines that do CPU-bound rules-engine work.withContext(Dispatchers.IO) {val request = Request.Builder().url("$baseUrl/v2/valuations/$vin").get().build()client.newCall(request).execute().use { response ->if (!response.isSuccessful) {throw ValuationProviderException(providerName, response.code)}val body: JsonNode = mapper.readTree(response.body?.string())ValuationResult(provider = providerName,vin = vin,estimatedValueCents = (body["valueUsd"].asDouble() * 100).toLong(),confidence = body["confidenceScore"].asDouble(),retrievedAt = Instant.now())}}}
Implements the interface for Black Book, wrapping a blocking OkHttp call in withContext(Dispatchers.IO); it can't run in-browser because it depends on a real OkHttpClient, a live network connection to Black Book, and a JSON parser, none of which the sandbox provides.
import com.fasterxml.jackson.databind.ObjectMapperimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.withContextimport okhttp3.OkHttpClientimport okhttp3.Requestimport java.time.Instantclass CarfaxClient(private val client: OkHttpClient,private val mapper: ObjectMapper,private val baseUrl: String) : ValuationProviderClient {override val providerName = "carfax"override suspend fun fetchValuation(vin: String): ValuationResult =withContext(Dispatchers.IO) {val request = Request.Builder().url("$baseUrl/history/valuation?vin=$vin").header("X-Carfax-Client", "vehicle-valuation-service").get().build()client.newCall(request).execute().use { response ->if (!response.isSuccessful) {throw ValuationProviderException(providerName, response.code)}// Carfax nests the number three levels deep and reports it in// whole dollars, not cents -- a quirk that lives and dies// inside this class instead of leaking into the domain model// or, worse, into how BlackBookClient parses its response.val history = mapper.readTree(response.body?.string())["history"]ValuationResult(provider = providerName,vin = vin,estimatedValueCents = history["estimatedValue"]["amountUsd"].asLong() * 100,confidence = history["estimatedValue"]["reliability"].asDouble(),retrievedAt = Instant.now())}}}
Implements the same interface for Carfax, whose response nests the price three levels deep in whole dollars instead of Black Book's flat, decimal shape; it can't run in-browser for the same reasons as the Black Book client: real HTTP, a real provider, and a real JSON parser.
🧠 Check your understanding
0/1 · 0/1 answered1. VehicleValuationService needs valuations from Black Book, Carfax, and S&P VIS, each with a different response shape and its own quirks. What is the main reason to give each provider its own ValuationProviderClient implementation instead of one class that branches on a provider name?