Evaluating Rules: The Engine Walkthrough
Sort once, evaluate every rule in priority order, let a hard stop win immediately, then cap the boosts — four small steps turn a pile of configured rules into one defensible price.
Everything so far has been building toward this: a `Rule` vocabulary from two lessons ago, and a way to load configured instances of it from CSV or Postgres from the last one. This lesson writes the last piece — the small, stable `RulesEngine` that actually runs the configured rules against one vehicle and produces a `PricingDecision`. This is the piece of the system that should almost never change, even as the rules underneath it change every week; if you find yourself editing `RulesEngine` to support a new business scenario, that is usually a sign the scenario belongs as a new `Rule` implementation instead.
The engine sorts its rules by priority exactly once, when it is constructed — not on every incoming valuation request. Sorting a few dozen rules is cheap in isolation, but this service is expected to answer thousands of valuation requests a minute (Datadog will show you the real number once Module 6 wires up metrics), and re-sorting the same fixed list on every one of those requests is pure waste. Evaluation itself does not stop at the first match, either: it runs every rule against the context and collects every non-null outcome with `mapNotNull`, because a single vehicle can legitimately trigger more than one boost — low mileage and a recent model year are entirely independent facts, and both should count.
Once every outcome is collected, the engine looks for a hard stop first: the first `Guardrail` or `NoBuy` in the list, regardless of what else fired. If one exists, the vehicle is refused immediately, full stop — no boost that also fired gets to soften that outcome, and no `NoOffer` gets to escalate it, because a hard stop is a stronger, more final signal than either. This is precisely the ordering guarantee priority exists to give you: because guardrails and no-buys sit at the front of the sorted list, a badly damaged vehicle triggers the `NoBuy` check early, but the actual short-circuit here happens by scanning the *collected* outcomes for a hard stop before doing anything else with the boosts — priority controls the order rules run in, and this hard-stop check controls what happens once they have.
If nothing hard-stopped the vehicle, the engine checks for a `NoOffer` next. This is the outcome that keeps a vehicle in play but refuses to let an algorithm set its price — an elevated but not disqualifying accident count is the canonical example. The result is a `PricingDecision.Escalated`, which in a real system would land on a human underwriter's queue rather than becoming an automated offer. Notice this check runs before boosts are totaled, for the same reason a hard stop does: there is no reason to compute a price a human is not going to see used.
Only once a vehicle clears both checks does the engine actually price it: sum every `Boost` outcome's amount, clamp the total with `coerceAtMost(maxBoostAmount)`, and add whatever survives the clamp to the provider's raw valuation. The cap is not decoration — it is a defense-in-depth measure against a `pricing_rule` table that someone (or some import script) has misconfigured. A single boost with a typo'd amount of `50000` instead of `500` should not be able to turn a data-entry mistake into a five-figure overpayment for one car; the cap guarantees the blast radius of a bad row in that table is bounded no matter how large the number in it is.
Trace one vehicle through the sample data below to see all four steps land: VIN-102 is 45,000 miles, a 2019 model, three reported accidents. Its outcomes are exactly one — `HighAccidentNoOffer` fires because three accidents falls in its 2-to-4 review range — and nothing else applies (it is neither old enough for the age guardrail nor damaged enough for the total-loss no-buy, and it is neither low-mileage nor new enough for either boost). No hard stop exists, so the engine moves to the `NoOffer` check, finds one, and returns `Escalated` without ever touching the boost math. Compare that to VIN-100 — new, low-mileage, no accidents — which clears every check and lands on an `Offer` with both of its boosts stacked and capped at $1,500.
Notice what this engine has not needed once in this entire lesson: a database connection, an HTTP client, or a mock of either. `RulesEngine.decide()` is pure Kotlin over an in-memory list and a data class — which means it is already fully unit-testable with nothing more than the stdlib you used to write the sample program below. That is deliberate, and it sets up exactly what comes next: the next lesson brings in Testcontainers, because the layers this engine sits between — the repository that loads `pricing_rule` rows, and the providers the raw valuation comes from — are exactly the parts of this service that do need a real Postgres and a real Mongo to test honestly.
// Continuing the Rule/RuleOutcome vocabulary from the previous lesson.// PricingDecision is what the engine hands back to the caller.sealed class PricingDecision {data class Offer(val amount: Double, val appliedBoosts: List<String>) : PricingDecision()data class Refused(val reason: String) : PricingDecision()data class Escalated(val reason: String) : PricingDecision()}class RulesEngine(rules: List<Rule>, private val maxBoostAmount: Double = 1_500.0) {// Sorted once, at construction time -- not on every valuation request.private val orderedRules = rules.sortedBy { it.priority }fun decide(context: ValuationContext): PricingDecision {val outcomes = orderedRules.mapNotNull { it.evaluate(context) }// Step 1: any hard stop -- Guardrail or NoBuy -- wins immediately.val hardStop = outcomes.firstOrNull { it is RuleOutcome.Guardrail || it is RuleOutcome.NoBuy }if (hardStop != null) {val reason = when (hardStop) {is RuleOutcome.Guardrail -> hardStop.reasonis RuleOutcome.NoBuy -> hardStop.reasonelse -> error("unreachable")}return PricingDecision.Refused(reason)}// Step 2: NoOffer defers to a human without refusing the vehicle outright.val noOffer = outcomes.filterIsInstance<RuleOutcome.NoOffer>().firstOrNull()if (noOffer != null) {return PricingDecision.Escalated(noOffer.reason)}// Step 3: boosts stack, but never past the cap.val boosts = outcomes.filterIsInstance<RuleOutcome.Boost>()val totalBoost = boosts.sumOf { it.amount }.coerceAtMost(maxBoostAmount)val finalAmount = context.providerValuation + totalBoostreturn PricingDecision.Offer(finalAmount, boosts.map { it.reason })}}
PricingDecision, the type the engine hands back to callers, and decide()'s four steps in isolation — continuing the Rule/RuleOutcome vocabulary from the previous lesson.
data class ValuationContext(val vin: String,val mileage: Int,val modelYear: Int,val accidentCount: Int,val providerValuation: Double,)sealed class RuleOutcome {data class Guardrail(val reason: String) : RuleOutcome()data class Boost(val amount: Double, val reason: String) : RuleOutcome()data class NoBuy(val reason: String) : RuleOutcome()data class NoOffer(val reason: String) : RuleOutcome()}interface Rule {val name: Stringval priority: Intfun evaluate(context: ValuationContext): RuleOutcome?}sealed class PricingDecision {data class Offer(val amount: Double, val appliedBoosts: List<String>) : PricingDecision()data class Refused(val reason: String) : PricingDecision()data class Escalated(val reason: String) : PricingDecision()}class RulesEngine(rules: List<Rule>, private val maxBoostAmount: Double = 1_500.0) {private val orderedRules = rules.sortedBy { it.priority }fun decide(context: ValuationContext): PricingDecision {val outcomes = orderedRules.mapNotNull { it.evaluate(context) }val hardStop = outcomes.firstOrNull { it is RuleOutcome.Guardrail || it is RuleOutcome.NoBuy }if (hardStop != null) {val reason = when (hardStop) {is RuleOutcome.Guardrail -> hardStop.reasonis RuleOutcome.NoBuy -> hardStop.reasonelse -> error("unreachable")}return PricingDecision.Refused(reason)}val noOffer = outcomes.filterIsInstance<RuleOutcome.NoOffer>().firstOrNull()if (noOffer != null) {return PricingDecision.Escalated(noOffer.reason)}val boosts = outcomes.filterIsInstance<RuleOutcome.Boost>()val totalBoost = boosts.sumOf { it.amount }.coerceAtMost(maxBoostAmount)val finalAmount = context.providerValuation + totalBoostreturn PricingDecision.Offer(finalAmount, boosts.map { it.reason })}}class MaxAgeGuardrail(override val priority: Int = 0) : Rule {override val name = "max-age-guardrail"override fun evaluate(context: ValuationContext): RuleOutcome? =if (context.modelYear < 2012) RuleOutcome.Guardrail("Model year ${context.modelYear} is too old to buy") else null}class TotalLossNoBuy(override val priority: Int = 1) : Rule {override val name = "total-loss-no-buy"override fun evaluate(context: ValuationContext): RuleOutcome? =if (context.accidentCount >= 5) RuleOutcome.NoBuy("Accident count ${context.accidentCount} indicates a total loss") else null}class HighAccidentNoOffer(override val priority: Int = 5) : Rule {override val name = "high-accident-no-offer"override fun evaluate(context: ValuationContext): RuleOutcome? =if (context.accidentCount in 2..4) RuleOutcome.NoOffer("Accident count ${context.accidentCount} requires manual review") else null}class LowMileageBoost(override val priority: Int = 10) : Rule {override val name = "low-mileage-boost"override fun evaluate(context: ValuationContext): RuleOutcome? =if (context.mileage < 30_000) RuleOutcome.Boost(500.0, "Mileage ${context.mileage} is under 30,000") else null}class RecentModelYearBoost(override val priority: Int = 11) : Rule {override val name = "recent-model-year-boost"override fun evaluate(context: ValuationContext): RuleOutcome? =if (context.modelYear >= 2023) RuleOutcome.Boost(1200.0, "Model year ${context.modelYear} is recent") else null}fun main() {val engine = RulesEngine(rules = listOf(LowMileageBoost(),MaxAgeGuardrail(),TotalLossNoBuy(),HighAccidentNoOffer(),RecentModelYearBoost(),),maxBoostAmount = 1_500.0,)val vehicles = listOf(ValuationContext("VIN-100", mileage = 12_000, modelYear = 2024, accidentCount = 0, providerValuation = 28_000.0),ValuationContext("VIN-101", mileage = 80_000, modelYear = 2010, accidentCount = 0, providerValuation = 5_000.0),ValuationContext("VIN-102", mileage = 45_000, modelYear = 2019, accidentCount = 3, providerValuation = 14_000.0),ValuationContext("VIN-103", mileage = 60_000, modelYear = 2018, accidentCount = 6, providerValuation = 9_000.0),)for (vehicle in vehicles) {println("${vehicle.vin} -> ${engine.decide(vehicle)}")}}
The complete engine, five configured rules, and a trace over four vehicles — run it and match the printed decisions against the walkthrough above.
Arena IDE🧠 Check your understanding
0/1 · 0/1 answered1. A vehicle in the same evaluation pass triggers a `NoBuy` outcome (from a total-loss rule) and a `Boost` outcome (from a low-mileage rule). What should `RulesEngine.decide()` return, and why?