Bringing It Together: One Request, Traced
Twenty-five lessons built one piece each — this last one follows a single VIN through every one of them, end to end, in one request.
There is no new API in this lesson. Everything you need is already built: a controller, a service, three provider clients, a rules engine, a Postgres-backed rule store, a Redis cache, a Mongo audit log, retry and circuit-breaker policies, a rate limiter, and a set of Datadog metrics. What this capstone does instead is trace one HTTP request — a `POST` to value a single VIN — through every one of those pieces in order, so you can see how they cooperate as a system rather than as twenty-six isolated lessons.
It starts where every request starts: `VehicleController` receives the VIN, does the minimal deserialization and validation work, and hands off to `VehicleValuationService.valuate(vin)` — a `suspend fun`, which matters immediately, because everything downstream of this call involves waiting on I/O, and a suspending function frees the request-handling thread to do other work instead of blocking on a network call or a database round trip.
The first thing the service does is check Redis, because the cache-aside pattern means the application, not the cache, owns the read-through logic: ask Redis for a cached valuation keyed by VIN, and on a hit, return immediately without ever touching a provider. On a miss — the normal path for a first-time VIN or one whose cached entry has aged out — execution falls through to the providers, and this is also where the rate limiter from the previous lesson gets consulted before any outbound call is attempted.
Assuming the token bucket has capacity, the service calls all three `ValuationProviderClient` implementations concurrently over OkHttp, each wrapped in the Failsafe retry-and-circuit-breaker policy built earlier in the course. A single dropped Carfax response gets quietly retried; a Carfax that has been failing consistently has its breaker open, so the service does not waste a timeout waiting on a provider that has already proven it is down, it fails fast and moves on with whichever quotes did come back. If the limiter did reject a call, that event is logged (sampled, not one line per rejection) and counted.
Whatever quotes make it back get blended into a valuation, and the result is written into Redis — the write half of cache-aside — so the next request for the same VIN, within the freshness window, never has to call a provider at all. The blended quotes are then handed to the config-driven rules engine, which applies guardrails, boosts, and no-buy/no-offer logic without a single hardcoded `if` scattered through the service — a pricing policy change is a configuration change, not a deploy.
The rules engine's decision is what gets returned to the caller, but the request is not actually finished: a coroutine is launched, deliberately not awaited, to write a `valuation_event` document to the Mongo audit log. It runs fire-and-forget on its own `SupervisorJob`-backed scope specifically so that a failure writing an audit record — which the caller does not need to succeed in order to receive a valid, already-computed decision — cannot fail or even slow down the response the caller is waiting on. That scope catches and logs its own errors, because nothing else is watching for them.
Wrapping the entire call, from the moment it entered `valuate()` to the moment it returns, is the Micrometer timer recording end-to-end latency, tagged by outcome; every provider call recorded its own success-or-failure counter; the circuit-breaker gauges reflect whichever provider, if any, currently has an open breaker; and if the limiter rejected anything along the way, that counter moved too. None of this required reading a single log line to know the request happened, roughly how long it took, and whether anything downstream was unhealthy while it ran — and the confidence that every piece of this path actually works the way it's described here came from Testcontainers proving the Postgres and Mongo behavior was real, and MockK proving the provider-client failure handling behaved correctly without ever making a real, billable call.
None of this is really about vehicles. Swap Black Book, Carfax, and S&P VIS for three shipping carriers quoting a delivery estimate, or three underwriting bureaus scoring a loan applicant, or three price feeds for a comparison engine, and the shape of the solution barely changes: call unreliable third parties safely, cache what's expensive to refetch, keep decision logic external and auditable, don't make the caller wait on work they don't need to wait on, and instrument every seam so the system can tell you when it's unhealthy before a customer does.
That is the actual thing this course was teaching. The vehicle-valuation service was never the point — it was the vehicle, no pun intended, for a set of patterns that show up in almost any service whose job is to aggregate answers from sources it does not control and hand back one decision it can stand behind. You now have all of it: the architecture, the safety mechanisms, the persistence choices, the concurrency model, and the observability to prove it's working. That's the whole course.
@RestController@RequestMapping("/api/vehicles")class VehicleController(private val valuationService: VehicleValuationService) {@PostMapping("/{vin}/valuation")suspend fun getValuation(@PathVariable vin: String): ResponseEntity<ValuationResponse> {val decision = valuationService.valuate(vin)return ResponseEntity.ok(ValuationResponse.from(decision))}}
The controller entry point: thin, suspending, and delegating everything to the service.
class VehicleValuationService(private val cache: ValuationCacheRepository,private val providers: List<ValuationProviderClient>,private val rulesEngine: RulesEngine,private val auditRepository: ValuationEventRepository,private val metrics: ProviderMetrics,private val auditScope: CoroutineScope,) {suspend fun valuate(vin: String): ValuationDecision = metrics.timeValuationRequest {cache.get(vin)?.let { cached -> return@timeValuationRequest cached }val quotes: List<ProviderQuote> = coroutineScope {providers.map { provider ->async {runCatching { provider.fetchValuation(vin) }.onSuccess { metrics.recordProviderCall(provider.name, "success") }.onFailure { metrics.recordProviderCall(provider.name, "failure") }.getOrNull()}}.awaitAll().filterNotNull()}val decision = rulesEngine.evaluate(vin, quotes)cache.put(vin, decision)auditScope.launch {runCatching {auditRepository.save(ValuationEvent.from(vin, quotes, decision))}.onFailure { ex ->logger.warn(ex) { "Failed to write valuation_event audit record for vin=$vin" }}}decision}}
The full traced path in one method: cache-aside, concurrent provider calls with per-call metrics, the rules engine, the cache write-back, and the fire-and-forget audit write — all wrapped in a request-latency timer.
@Configurationclass AuditCoroutineConfig {@Beanfun auditScope(): CoroutineScope =CoroutineScope(SupervisorJob() + Dispatchers.IO + CoroutineName("valuation-audit-writer"))}
Why the audit write runs on its own SupervisorJob-backed scope: one failed write must never cancel sibling work or the request that triggered it.
🧠 Check your understanding
0/1 · 0/1 answered1. In the traced request, the write to the Mongo valuation_event audit log happens on a separate coroutine launched with a SupervisorJob, wrapped in its own runCatching. Why not simply let an exception there propagate up and fail the whole request?