Por Qué las Cadenas de if No Escalan
Cada guardrail que pide el negocio aterriza como un `if` más en `valuate()` — hasta que la función tiene 200 líneas que nadie se atreve a tocar.
Abre VehicleValuationService.valuate() ocho meses después de iniciar este proyecto y no encontrarás la función limpia del Módulo 1. Encontrarás una función que empieza igual — llama a los proveedores, toma el número de Black Book como base — y después sigue y sigue: una comprobación para alto kilometraje en autos viejos, una comprobación para demasiados accidentes reportados, una comprobación para estados con riesgo de inundación, una comprobación para versiones híbridas que superan cierto kilometraje, un pequeño ajuste al alza para autos casi nuevos con poco kilometraje. Cada una de esas comprobaciones llegó de la misma forma: un mensaje de Slack del equipo de pricing, un PR, una revisión, un deploy. Nadie diseñó esta función para que se viera así. Se fue acumulando.
La forma del daño es siempre la misma. Cada guardrail es un `if` pequeño y autocontenido que retorna temprano o ajusta el precio — perfectamente razonable en aislamiento — pero es la acumulación lo que mata la función. Un `if` de cinco líneas sobre títulos de inundación en Florida queda al lado de un `if` de tres líneas sobre riesgo de batería en híbridos, que a su vez queda al lado de un guardrail de kilometraje y año de una conversación completamente distinta ocho sprints atrás. Ninguno hace referencia a los demás, ninguno está ordenado a propósito, y un comentario como `// agregado después de que el piloto de híbridos perdiera dinero` es el único registro de por qué existe esa rama. Quien sea que toque esta función después tiene que leer cada guardrail anterior solo para asegurarse de que el nuevo no interactúe en silencio con uno viejo.
El problema real no es el if — es el costo de redeploy que lleva pegado. El equipo de pricing no piensa en Kotlin; piensa en umbrales y montos en dólares, y esos cambian con un ritmo de negocio que no tiene nada que ver con tu ritmo de sprint. "Sube el corte de kilometraje de 120,000 a 100,000 porque los datos de Carfax se volvieron ruidosos en camionetas de alto kilometraje este trimestre" es una petición de una sola frase. En un código basado en cadenas de if, satisfacerla significa editar un literal enterrado en una función de 200 líneas, abrir un PR, esperar revisión y CI, y enviar un deploy completo — por un solo número. Si ese número está mal y el negocio lo nota un viernes a las 2pm, la solución es el mismo viaje de varias horas, y mientras tanto el servicio sigue haciendo las ofertas equivocadas.
Esta es la idea central hacia la que construye el resto de este módulo: sacar la lógica de decisión del código y representarla como datos. Un guardrail no es fundamentalmente una sentencia if de Kotlin — es un hecho: "si el kilometraje es mayor a 120,000 y el año del modelo es anterior a 2015, no comprar", expresado como un campo, un operador, un umbral y un resultado. Una vez que una regla es datos — una fila en un CSV, una fila en una tabla de Postgres — cambiar el umbral de 120,000 a 100,000 es editar un valor, no cambiar código. El código Kotlin que permanece fijo es el *motor* que lee esos datos y los aplica de forma consistente, y ese motor es exactamente el tipo de código pequeño, bien probado y que rara vez cambia que quieres tener entre el negocio y producción.
Ayuda pensarlo de la misma forma en que piensas un archivo de configuración frente a una recompilación. Nadie espera redesplegar un servicio para cambiar un nivel de log o un feature flag — ese valor vive fuera del binario precisamente porque cambia en un calendario distinto al del código que lo rodea. Los umbrales de precio, los montos de boost y las condiciones de no compra son el mismo tipo de valor: son política, no lógica, y una política que cambia semanalmente no tiene nada que hacer soldada a un artefacto de la JVM que cambia con el tren de releases.
Esto no significa escribir cero código, y no es gratis. Un motor de reglas cambia un problema por otro distinto y mejor. En lugar de leer una función de 200 líneas de arriba a abajo, un ingeniero ahora lee un evaluador mucho más pequeño y genérico — pero también tiene que confiar en que los *datos* que lo alimentan son correctos, lo que significa que esos datos necesitan su propia validación, su propio proceso de revisión e, idealmente, sus propias pruebas. Ese cambio vale la pena para lógica que de verdad cambia con un ritmo de negocio. No vale la pena para una validación de entrada puntual que nunca se va a volver a tocar; no toda condicional de tu código merece convertirse en una fila de una tabla.
Las próximas tres lecciones construyen exactamente esto: una interfaz `Rule` pequeña y una jerarquía sellada de resultados que Kotlin puede razonar en tiempo de compilación, una forma de cargar reglas concretas desde un archivo CSV y desde una tabla de Postgres, y el motor de evaluación que convierte un montón de reglas configuradas en una única `PricingDecision` defendible para un vehículo. Nada de esto es exótico — es el mismo patrón que la suscripción de seguros, la detección de fraude y los motores de descuento han usado durante décadas, aplicado a la pregunta que este servicio existe para responder: compramos este auto, y por cuánto.
// 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.
🧠 Comprueba tu comprensión
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?