Decision Log¶
Registro de decisiones técnicas y de diseño. Cada entrada documenta qué se eligió, qué alternativas se consideraron y por qué se tomó esa decisión.
DL-001 — Una sola tabla player_cards¶
Decisión: Una única tabla para todas las cartas de todos los jugadores, con índice en
owner_id.
Alternativas consideradas:
- CockroachDB para escalado horizontal
Motivo:
Una tabla grande no es lenta por sí sola, es lenta sin los índices correctos. PostgreSQL con
un índice en owner_id maneja cientos de millones de filas sin problema. Las consultas del
tipo "dame las cartas de este jugador" van directamente al índice y no escanean la tabla entera.
CockroachDB tiene sentido cuando se necesita escalar escrituras en múltiples regiones geográficas simultáneamente. Añadirlo en v1 sería over-engineering y añadiría complejidad operacional innecesaria.
DL-002 — last_reward en users en vez de tabla daily_rewards¶
Decisión: Añadir el campo last_reward TIMESTAMP DEFAULT '2000-01-01' directamente
en la tabla users.
Alternativas consideradas:
- Tabla separada
daily_rewardsconuser_idylast_reward
Motivo:
La lógica de comprobación es now() - last_reward > 24h — un solo campo, una sola comparación,
sin JOINs. Menos tablas, menos complejidad, misma funcionalidad.
El DEFAULT '2000-01-01' hace que la primera comprobación siempre sea verdadera sin casos
especiales en el código — el primer sobre gratis sale de esta misma lógica sin ningún campo
extra.
DL-003 — Caché de pools de cartas con Spring Cache + Caffeine¶
Decisión: Cachear en memoria el pool de cartas por pack_type usando Spring Cache
Abstraction con Caffeine como implementación.
Alternativas consideradas:
- Consultar
pack_type_cardsen cada apertura de sobre - Redis como caché distribuida
Motivo:
El pool de cartas de un pack no cambia entre aperturas, por lo que consultar la DB cada vez es trabajo
innecesario. Con @Cacheable, la query se ejecuta una sola vez y el resultado vive en RAM.
En hardware limitado (ej.: Raspberry Pi), el cuello de botella es el I/O de disco, no la CPU. Reducir queries a PostgreSQL por request es especialmente importante en este contexto.
Spring Cache Abstraction permite migrar a Redis en el futuro sin tocar el código de negocio — si el proyecto crece y necesita caché distribuida, el cambio es de configuración, no de lógica.
DL-004 — Guardar seeds de aperturas de sobres¶
Decisión: Guardar el seed de cada apertura en la tabla pack_opens.
Alternativas consideradas:
- No guardar el seed
Motivo: El seed permite reproducir exactamente cualquier apertura. Esto tiene dos usos concretos:
- Auditoría de fairness: si un jugador reporta un comportamiento sospechoso, se puede verificar que la apertura fue legítima.
- Debugging: si hay un bug en el generador, se puede reproducir el problema exacto.
Es también un argumento de transparencia hacia la comunidad, ya que el sistema es verificable.
DL-005 — REJECTED en vez de borrar trade offers¶
Decisión: Cuando el dueño de un request acepta una oferta, las demás pasan a estado
REJECTED en vez de borrarse.
Alternativas consideradas:
- Borrar las ofertas rechazadas de la DB
Motivo: Borrar datos de trades elimina la capacidad de auditoría e historial. Si hay una disputa entre jugadores o un bug en el swap, los registros deben existir para poder investigar.
REJECTED cuesta un byte adicional por fila y a cambio proporciona trazabilidad completa
de todas las interacciones de trading.
DL-006 — Java decide el pool, Rust solo sortea¶
Decisión: Java consulta pack_type_cards y pasa el pool a Rust. Rust no tiene acceso
a la DB ni decide qué cartas pertenecen a qué pack.
Alternativas consideradas:
- Rust lee un archivo de configuración con el pool de cada pack
- Rust tiene su propia DB local
Motivo: La lógica de negocio (qué cartas pertenecen a qué pack) debe vivir donde vive el resto de la lógica de negocio: en Java, con acceso a PostgreSQL. Rust tiene una responsabilidad única y bien definida: dado un pool y una seed, sortear. Esto hace el binario Rust más simple, más testeable y más fácil de mantener.
Añadir una DB local a Rust o archivos de configuración duplicaría la fuente de verdad y crearía problemas de sincronización.
DL-006 — rarities como tabla en vez de ENUM de PostgreSQL¶
Decisión: Tabla rarities separada con name y ev_points.
Alternativas consideradas:
- ENUM de PostgreSQL (
COMMON,UNCOMMON,RARE,LEGENDARY,MYTHIC)
Motivo:
Un ENUM de PostgreSQL requiere una migración para añadir valores nuevos (ALTER TYPE).
Una tabla solo requiere un INSERT.
Además, ev_points por rareza vive naturalmente en esta tabla: si cambian los puntos de una
rareza, es un UPDATE de una fila, no un cambio de código.
DL-008 — ENUMs de PostgreSQL para estados de trading¶
Decisión: trade_status y offer_status como ENUMs de PostgreSQL en vez de VARCHAR.
Alternativas consideradas:
- VARCHAR con validación en la aplicación
- Integer con constantes en el código
Motivo: Los ENUMs de PostgreSQL son más eficientes en almacenamiento que VARCHAR (se guardan como integers internamente) y añaden validación a nivel de base de datos — es imposible insertar un estado inválido aunque haya un bug en la aplicación. Son también autodocumentados: cualquier persona que inspeccione el schema entiende los estados posibles sin consultar el código.
A diferencia de rarities, los estados de trading son un conjunto cerrado que no se espera
que cambie — por eso aquí sí tiene sentido el ENUM.
DL-009 — pack_type_cards como tabla de relación¶
Decisión: Tabla pack_type_cards para relacionar packs con su pool de cartas disponibles.
Alternativas consideradas:
- Campo
pack_typeenbase_cards(una carta pertenece a un solo pack) - Hardcodear los pools en el código
Motivo: Una carta puede aparecer en múltiples packs (una carta de espacio puede estar en el pack estándar y en un pack especial de aniversario). La relación es muchos-a-muchos, y una tabla de relación es el modelo correcto.
Hardcodear los pools en el código o en archivos de configuración de Rust crearía una fuente de verdad separada de la DB, con todos los problemas de sincronización que eso implica.
Con esta tabla, añadir un pack de temporada es solo un conjunto de INSERTs — sin cambios de código ni despliegues.
DL-010 — Cartas con EVs fijos¶
Decisión: Los campos ev_hp, ev_atk, ev_def, ev_spd se eliminan del modelo.
Los stats de una carta quedan fijados en el momento de generación y no pueden modificarse
posteriormente por el jugador.
Alternativas consideradas:
- Sistema de EVs editable: el jugador distribuye puntos extra por raridad tras obtener la carta
- EVs fijos generados por Rust junto al resto de stats al abrir el sobre
DL-011 — Rust devuelve solo (card_id, rarity)¶
Decisión: El binario Rust es responsable únicamente de decidir qué carta sale y con qué rareza. El cálculo de stats finales se realiza en Spring Boot, que tiene acceso a los datos base y a los multiplicadores de rareza.
Alternativas consideradas:
- Rust calcula stats completas: requiere pasarle todo el card pool con stats base y multiplicadores, acoplando lógica de negocio al binario y dificultando cambios futuros sin recompilación
Consecuencias:
- El contrato de salida de Rust es mínimo:
[{ card_id, rarity }, ...] - Los multiplicadores de rareza viven en Spring Boot (constantes o tabla de configuración)
- Cambiar la fórmula de stats no requiere recompilar el generador
DL-012 — Rareza como multiplicador, no atributo fijo de carta¶
Decisión: Las cartas no tienen rareza fija. Cualquier carta puede salir en cualquier rareza
al abrir un sobre. Las stats base (stat_x_base, stat_x_range) definen el potencial de la
carta; la rareza actúa como multiplicador aplicado en el momento de generación.
Alternativas consideradas:
- Rareza fija por carta (rarity_id en base_cards): más simple, pero impide que una misma carta aparezca en distintas rarezas según el pack o la suerte del jugador
Consecuencias:
rarity_idse elimina debase_cardsplayer_cardsañaderarity_idpara registrar con qué rareza salió esa instancia concreta- La tabla
raritiesse mantiene como lookup table con los multiplicadores
DL-013 — Stats base definidas como (min, avg, max)¶
Decisión: Cada stat de una carta base se define con tres valores explícitos: mínimo, promedio y máximo. Al generar una instancia, Spring Boot samplea una distribución normal centrada en avg con los extremos como límites, y aplica el multiplicador de rareza sobre el resultado.
Alternativas consideradas:
- base + range (schema anterior): menos expresivo, el promedio queda implícito y no permite distribuciones asimétricas
- Un único valor base sin variación: más simple pero todas las instancias de la misma carta serían idénticas dentro de la misma rareza