Skip to content

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_rewards con user_id y last_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_cards en 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:

  1. Auditoría de fairness: si un jugador reporta un comportamiento sospechoso, se puede verificar que la apertura fue legítima.
  2. 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_type en base_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_id se elimina de base_cards
  • player_cards añade rarity_id para registrar con qué rareza salió esa instancia concreta
  • La tabla rarities se 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