Why Hardcoded if Chains Don't Scale
Every guardrail the business asks for lands as one more `if` in `valuate()` — until the function is 200 lines nobody dares to touch.
Open VehicleValuationService.valuate() eight months into this project and you will not find the clean function from Module 1. You will find a function that starts the same way — call the providers, take Black Book's number as the base — and then keeps going: a check for high mileage on old cars, a check for too many reported accidents, a check for flood-risk states, a check for hybrid trims past a mileage cliff, a small bump for near-new low-mileage cars. Every one of those checks arrived the same way: a Slack message from the pricing team, a PR, a review, a deploy. Nobody designed this function to look like this. It accreted.
The shape of the damage is always the same. Each guardrail is a small, self-contained `if` that returns early or nudges the price — perfectly reasonable in isolation — but the accumulation is what kills the function. A five-line `if` about Florida flood titles sits next to a three-line `if` about hybrid battery risk, which sits next to a mileage-and-year guardrail from a completely different conversation eight sprints ago. None of them reference each other, none of them are ordered on purpose, and a comment like `// added after the hybrid pilot lost money` is the only record of why a given branch exists at all. Whoever touches this function next has to read every prior guardrail just to be sure their new one does not silently interact with an old one.
The real problem is not the if statement — it's the redeploy cost attached to it. The pricing team does not think in Kotlin; they think in thresholds and dollar amounts, and those change on a business cadence that has nothing to do with your sprint cadence. "Bump the mileage cutoff from 120,000 to 100,000 because Carfax data got noisy on high-mileage trucks this quarter" is a one-sentence request. In an if-chain codebase, satisfying it means editing a literal buried in a 200-line function, opening a PR, waiting for review and CI, and shipping a full deploy — for a single number. If that number is wrong and the business notices at 2pm on a Friday, the fix is the same multi-hour round trip, and in the meantime the service keeps making the wrong offers.
This is the core idea the rest of this module builds toward: pull the decision logic out of the code and represent it as data. A guardrail is not fundamentally a Kotlin `if` statement — it is a fact: "if mileage is greater than 120,000 and model year is before 2015, refuse to buy," expressed as a field, an operator, a threshold, and an outcome. Once a rule is data — a row in a CSV, a row in a Postgres table — changing the threshold from 120,000 to 100,000 is an edit to a value, not a code change. The Kotlin code that stays fixed is the *engine* that reads that data and applies it consistently, and that engine is exactly the kind of small, well-tested, rarely-changing code you want sitting between the business and production.
It helps to think of this the same way you think about a config file versus a recompile. Nobody expects to redeploy a service to change a log level or a feature flag — that value lives outside the binary precisely because it changes on a different schedule than the code around it. Pricing thresholds, boost amounts, and no-buy conditions are the same kind of value: they are policy, not logic, and policy that changes weekly has no business being welded into a JVM artifact that changes on a release train.
This does not mean writing zero code, and it is not free. A rules engine trades one problem for a different, better one. Instead of reading a 200-line function top to bottom, an engineer now reads a much smaller, generic evaluator — but they also have to trust that the *data* driving it is correct, which means the data needs its own validation, its own review process, and ideally its own tests. That trade is worth making for logic that legitimately changes on a business cadence. It is not worth making for a one-off input validation check that will never be touched again; not every conditional in your codebase deserves to become a row in a table.
The next three lessons build exactly this: a small `Rule` interface and a sealed hierarchy of outcomes Kotlin can reason about at compile time, a way to load concrete rules from a CSV file and from a Postgres table, and the evaluation engine that turns a pile of configured rules into a single, defensible `PricingDecision` for one vehicle. None of it is exotic — it is the same pattern insurance underwriting, fraud detection, and discount engines have used for decades, applied to the question this service exists to answer: do we buy this car, and for how much.
// The valuate() function after eight months of one-off guardrails --// nobody wants to touch it, and nobody can tell at a glance which// checks matter or in what order.fun valuate(facts: VehicleFacts): Double {val base = rawProviderValuation(facts).amountif (facts.mileage > 120_000 && facts.modelYear < 2015) {return 0.0}if (facts.accidentCount >= 3) {return 0.0}if (facts.state == "FL" && facts.modelYear < 2013 && facts.mileage > 90_000) {// flood-title risk on older Florida cars, added after a Q2 chargebackreturn 0.0}if (facts.trim == "Hybrid" && facts.mileage > 100_000) {// battery replacement risk, added after the hybrid pilot lost moneyreturn base * 0.85}if (facts.mileage < 30_000 && facts.modelYear >= 2022) {return base * 1.05}return base}
The valuate() function after eight months of one-off guardrails — nobody wants to touch it, and nobody can tell at a glance which checks matter or in what order.
// The same guardrails, expressed as data instead of code -- this is the// shape lessons 19 through 21 turn into a working Rule and a small engine.data class RuleRow(val ruleType: String, // "GUARDRAIL", "BOOST", "NO_BUY", "NO_OFFER"val field: String, // "mileage", "modelYear", "accidentCount", ...val operator: String, // ">", "<", ">=", "<=", "=="val threshold: Double,val priority: Int,)// The mileage-and-year guardrail from valuate() above, expressed as a// row instead of an if statement.val mileageGuardrail = RuleRow(ruleType = "GUARDRAIL",field = "mileage",operator = ">",threshold = 120_000.0,priority = 0,)
The same guardrails, expressed as data instead of code — this is the shape lessons 19 through 21 turn into a working Rule and a small evaluation engine.
🧠 Check your understanding
0/1 · 0/1 answered1. The pricing team wants to change a guardrail's mileage cutoff from 120,000 to 100,000 miles. In a codebase where that guardrail is one more `if` inside `valuate()`, what does shipping that change require — and what is the rules-as-data approach meant to fix?