Elegir el Almacén Correcto para el Trabajo Correcto
Cuatro preguntas — consistencia, proporción de lectura/escritura, estabilidad de esquema, patrón de consulta — bastan para señalar casi cualquier tabla nueva hacia el almacén al que realmente pertenece.
Las últimas tres lecciones recorrieron tres decisiones concretas — Postgres para las reglas, Redis para la caché de valuación, MongoDB para el registro de auditoría — cada una justificada en sus propios términos. Esta lección separa el razonamiento detrás de las tres de las particularidades de este servicio, para que viaje contigo hasta la próxima tabla que tengas que diseñar, toque o no un vehículo.
Cuatro preguntas hacen la mayor parte del trabajo. Primero: ¿qué tan fuerte necesita ser la consistencia, y necesita más de una fila cambiar junta de forma atómica? `pricing_rule` necesitaba ambas cosas — un guardrail y un boost cambiando como una sola unidad, con una llave foránea que jamás debe apuntar a la nada. Esa necesidad de atomicidad multi-fila y garantías referenciales es la señal más fuerte de todo este checklist de que un dato pertenece a una base de datos relacional; nada más en esta lista la anula si la respuesta es 'sí, y equivocarse aquí causa daño real'.
Segundo: ¿cuál es la proporción de lectura/escritura, y qué tan costosa es la escritura que está reemplazando? La caché de valuación se lee muchísimo más a menudo que el camino costoso (tres llamadas a proveedores) frente al que se coloca, y cada lectura que satisface es una lectura que evita que ocurra por el camino costoso. Esa asimetría — barato de servir, costoso de recalcular, tolerante a la obsolescencia — es la firma de un problema de caché, y vale la pena notar qué la descalificaría: si la obsolescencia fuera inaceptable (el saldo de un banco, un conteo de inventario al momento de pagar), ninguna proporción de lectura/escritura justificaría un patrón cache-aside, porque el valor entero de ese patrón descansa en que 'una respuesta un poco vieja sigue siendo una buena respuesta'.
Tercero: ¿qué tan estable es el esquema, y quién más depende de que su forma se mantenga fija? Las columnas de `pricing_rule` apenas se mueven — un porcentaje `NUMERIC`, una ventana de fechas, una llave foránea — mientras que `valuation_event` ganó un puntaje de fraude y un código de promoción sin que nadie tuviera que pedir permiso. Cuando la forma de una tabla es genuinamente estable y compartida entre sistemas que necesitan estar de acuerdo sobre ella, la rigidez de un esquema relacional es una virtud, atrapando errores en el momento de escribir. Cuando se espera que un payload siga ganando campos nuevos y opcionales a medida que el negocio hace preguntas nuevas, forzar cada una de esas adiciones a través de una migración coordinada es fricción sin un beneficio de seguridad correspondiente — que es precisamente el caso a favor del almacén documental.
Cuarto: ¿cómo se ve realmente la consulta? `SELECT ... WHERE category = ? AND la ventana de vigencia cubre hoy ORDER BY priority` es una consulta relacional en su esencia — filtrar y ordenar sobre columnas conocidas, posiblemente unida contra otra tabla. `GET valuation:$vin` es una búsqueda pura por llave sin ninguna lógica de filtrado — la interfaz entera de una caché. `INSERT este documento, y luego búscalo por ID o por un puñado de campos predecibles` es la forma nativa de un almacén documental. Si te encuentras diseñando lógica elaborada del lado de la aplicación para simular joins sobre una caché o una colección documental, eso suele ser una señal de que el dato pertenece a otro lugar, no una señal de que necesitas una llave de caché más ingeniosa.
Ninguna de estas cuatro preguntas tiene una respuesta universalmente correcta — solo tienen una respuesta correcta para una porción específica de datos haciendo un trabajo específico, que es exactamente por qué un solo servicio puede y debe usar tres bases de datos a la vez sin que eso sea señal de desorganización. La tabla de abajo son las respuestas de este servicio; las cuatro preguntas de arriba son lo que las produjo, y son lo que deberías volver a preguntarte para la próxima tabla que diseñes, en este servicio o en cualquier otro.
Una advertencia que vale la pena llevar hacia el Módulo 4: nada de esto cambia porque las llamadas a estos almacenes ocurran desde dentro de una función suspend. Las corrutinas hacen barato disparar una escritura a Mongo sin bloquear un hilo, o esperar (await) una búsqueda en Redis junto a tres llamadas a proveedores — pero no cambian a qué almacén pertenece el dato. La decisión de este módulo y las técnicas de concurrencia del siguiente son ejes independientes: primero acierta el almacén, después haz que las llamadas hacia él sean rápidas.
// Data | Store | Deciding factor(s) from the checklist// ---------------------|-----------|----------------------------------------------------// pricing_rule | Postgres | Multi-row atomicity + foreign keys to vehicle_category// valuation lookup | Redis | High read/write ratio, expensive recompute, tolerable staleness// valuation_event | MongoDB | High write volume, evolving optional fields, lookup-by-ID/filter//// Ask, for any new table:// 1. Does more than one row need to change together, atomically? -> leans relational// 2. Is this read far more than written, and is a stale read fine? -> leans cache// 3. Is the shape stable and shared, or evolving and single-owner? -> stable=relational, evolving=document// 4. Is the query a join+filter, a pure key lookup, or fetch-by-ID? -> matches the store's native shape
This is illustrative only (runnable: false) — the recap table synthesizing which store this service picked for each job and the deciding factor from the checklist, meant to be read rather than executed.
suspend fun valuate(vin: String): ValuationResult {// Q2 answer: cache-aside read (Redis) — cheap to serve, expensive to recomputevaluationCache.get(vin)?.let { return it }// Q1 answer: transactional, foreign-keyed read (Postgres)val rules = ruleRepository.findActiveRulesForCategory(categoryOf(vin))val result = computeValuation(vin, rules, providerClients)valuationCache.set(vin, result, ttlWithJitter())// Q3/Q4 answer: append-only, schema-flexible write (MongoDB)eventLog.save(ValuationEvent.from(vin, rules, result))return result}
This is illustrative only (runnable: false) — a single suspend function showing all three stores queried from one code path, the concrete payoff of the checklist: the caller doesn't know or care which store answered.
🧠 Comprueba tu comprensión
0/1 · 0/1 answered1. A new requirement: track a running 'total number of offers made per dealer, updated in real time, viewed on a live dashboard, where a dealer's count must never be off by even one.' Using the four-question checklist, which store fits best, and why?