Un juego online, decenas de miles de jugadores y puntos llegando de todas partes: de las partidas y también de su marketplace de NFTs, con eventos nacidos en blockchain y servidos por Kafka. Cada jugador competía en varias tablas a la vez y los torneos disparaban el tráfico. El motor lo digiere todo, y cuando toca corregir puntos, recalcula sin descuadrar una sola tabla.
Casos
Rails 7, MongoDB, Kafka, Sidekiq, Redis, Node.js, WebSocket, PHP, Docker
Una base de 30-40.000 jugadores y una media de 50-100 eventos por segundo, que en torneos se disparaba. Cada acción relevante debía reflejarse en las clasificaciones: las partidas del juego, sí, pero también la actividad en su marketplace, que otorgaba puntos por participar. Y todo servido por API a las aplicaciones, al instante.
La trampa de los leaderboards es pensar que son un sumatorio. Aquí no:
Aquí viene lo distinto: buena parte de los eventos nacían en una blockchain. El juego y su marketplace de NFTs funcionaban sobre contratos inteligentes (compras, ventas, recompensas de partida) y cada acción quedaba emitida como evento on-chain. Unos listeners escuchaban esos contratos, normalizaban lo que veían y lo publicaban en Kafka; el motor consumía los topics, clasificaba por fuente y persistía cada evento como un hecho.
El evento es la verdad; las tablas son cálculo derivado. Esa decisión permite corregir: si un evento se anula, se marca y las tablas afectadas se recalculan de los hechos. Y con blockchain de por medio tiene una gracia extra: los eventos llegan cuando la red los confirma, no cuando ocurren: el motor tenía que encajar hechos que aparecen con retraso y en desorden sin descuadrar ninguna tabla.
{
"source": "marketplace",
"wallet": "0x9f…c41",
"action": "sale_completed",
"points": 12,
"occurred_at": "2023-04-16T19:22:31Z"
}
Con decenas de tablas × periodos × 40.000 jugadores, la pregunta "¿cómo va la semanal?" es una agregación seria de MongoDB: filtrar millones de eventos por ventana temporal, agrupar por jugador, aplicar el reglamento de esa tabla, ordenar y paginar. Aquí nació el Aggregator de pipelines componibles que hoy uso en más proyectos (está en mi biblioteca de Rails):
Aggregate::Aggregator.new(Events::BattleArena)
.add_stage(:match, { occurred_at: { "$gte": week_start, "$lt": week_end } })
.add_stage(:group, { _id: "$wallet", points: { "$sum": "$points" } })
.add_stage(:sort, { points: -1 })
.apply
Los recálculos pesados corrían como pools de jobs de Sidekiq, lotes en paralelo, con espera coordinada de que el lote completo terminara antes de publicar la tabla nueva.
No todas las tablas son igual de vivas. Las históricas y los máximos de referencia cambian despacio: recalcularlos por petición sería quemar CPU para dar siempre el mismo resultado. Ahí entra Redis con invalidación: el dato se sirve de caché y solo se recalcula cuando algo lo toca.
maximus_data = redis.get("#{leaderboard}/#{period}") # máximos cacheados
# ... y al reprocesar eventos de ese periodo, la clave se invalida
Todo se expone por una API RESTful: fuente, tabla, periodo, paginación, y metadatos (máximos, cortes temporales, totales) para que las apps pinten cada clasificación sin lógica propia. Un contrato único para decenas de tablas.
Lo caro de un leaderboard no es sumar, es volver a sumar. Las correcciones y los reprocesos mandaron en el diseño: eventos como hechos inmutables marcables, cálculos derivados reproducibles y lotes que se pueden relanzar. Si tu clasificación no sabe rectificar, no está terminada; los torneos con premio lo dejan claro rapidísimo.
Un motor que digirió años de partidas, torneos con picos de tráfico y cambios de reglas, sirviendo clasificaciones al instante a decenas de miles de jugadores. Y de regalo, piezas (agregaciones componibles, pools de jobs) que siguen trabajando hoy en otros proyectos.