Lorenzo SanseverinoThe Last Developer
IT EN
Portfolio

Backend

Beebot

A long-running personal project: a Discord bot on one side, a League of Legends statistics engine on the other.

FIRST COMMIT
3 February 2022
EVOLUTION
Discord bot → LoL API
DATA
MongoDB + Redis
STACK
Java, Spring, JDA

Origins

First, the people in the server. Then, the data engine.

Beebot’s first commit dates to 3 February 2022. It starts as a Java Discord bot: server commands, audio playback and, soon afterwards, custom sound uploads. It did not begin as a fully planned statistics platform. Its capabilities grew alongside the way people used the bot.

League of Legends gradually becomes a domain of its own. Early summoner, champion and rune commands are followed by LP tracking in 2024. Once the tracker stores more than ladder progress, its name changes too: LPTracker becomes MatchTracker.

The important shift is from displaying external data to building a history of its own. Persisted matches, derived statistics and builds can serve more than one interface. Today Discord commands and HTTP APIs share domain services; Iodid consumes those APIs in the browser.

How it works

Behind a profile request.

These scenarios illustrate the logical flow between cache, persistence and Riot jobs. Timings are illustrative; HTTP responses distinguish available, partial and pending data.

beebot / api trace
Scenario
  1. API
  2. Redis
  3. MongoDB
  4. Riot
  1. GET /api/lol/EUW1/profile/{puuid}
  2. REDIS GET beebot:lol:ls:EUROPE:EUW1:{puuid}:summoner HIT · 2 ms
  3. 200 SummonerView 6 ms
  1. API
  2. Redis
  3. MongoDB
  4. Riot
  1. GET /api/lol/EUW1/profile/{puuid}
  2. REDIS GET beebot:lol:ls:EUROPE:EUW1:{puuid}:summoner MISS · 1 ms
  3. MONGO summoner.findOne({ puuid }) HIT · 9 ms
  4. REDIS SET beebot:lol:ls:EUROPE:EUW1:{puuid}:summoner 1 ms
  5. 200 SummonerView 15 ms
  1. API
  2. Redis
  3. MongoDB
  4. Riot
  1. GET /api/lol/EUW1/profile/{puuid}
  2. REDIS GET beebot:lol:ls:EUROPE:EUW1:{puuid}:summoner MISS · 1 ms
  3. MONGO summoner.findOne({ puuid }) MISS · 8 ms
  4. QUEUE QueueHandler.immediate(...) → RiotScheduler QUEUED
  5. 202 profile_pending 11 ms
  6. RIOT GET /riot/account/v1/accounts/by-puuid/{puuid} 200 · 141 ms
  7. RIOT GET /lol/summoner/v4/summoners/by-puuid/{puuid} 200 · 152 ms
  8. MONGO summoner.insertOne(profile) 7 ms
  9. REDIS SET beebot:lol:ls:EUROPE:EUW1:{puuid}:summoner 1 ms
  10. 200 SummonerView RETRY · 6 ms
  1. API
  2. Redis
  3. MongoDB
  4. Riot
  1. GET /api/lol/EUW1/profile-by-name/Player/EUW
  2. REDIS Riot ID lookup · Player#EUW · EUW1 MISS · 1 ms
  3. MONGO saved summoner lookup by Riot ID MISS · 8 ms
  4. RIOT GET /riot/account/v1/accounts/by-riot-id/Player/EUW 404 · 138 ms
  5. 404 not_found 149 ms

Example with indicative timings. Riot calls always go through the queue.

Framework

One Java app, four layers.

App boots the Spring API, the schedulers and the JDA bot together. Discord commands and HTTP controllers hold no domain logic: both go through the same services, which hand slow work to queues and schedulers.

ENTRY

JDA for Discord commands, Spring on embedded Tomcat for the APIs.

SERVICES

Profiles, matches, champions and leaderboards, shared by bot and API.

JOBS

Queues and schedulers for Riot calls, tracking and deferred computation.

DATA

Canonical MongoDB, derived Redis, MariaDB for guilds and settings.

Logic

From player identity to an HTTP response.

  1. A name for discovery, a PUUID for identity

    Search accepts a Riot ID or prefix and an optional shard. Results preserve region and player identity. After resolution, PUUID is the stable key for profiles and matches: a display-name change does not create a new player.

  2. Serve available data and prepare missing components

    Services read Redis caches and persisted MongoDB data. If identity resolution or the base profile is still missing, the API returns 202. Once the base exists, it can return 200 with a partial view and metadata.refresh=true. A client must not turn “being prepared” into zero-valued statistics.

  3. A new request must not duplicate an active job

    The job registry deduplicates active work by type, logical route and key. RiotScheduler separates upstream calls by shard, ComputeScheduler handles analysis and SyncScheduler coordinates synchronization. A more urgent request can promote queued work without launching a second copy.

  4. Match history has a separate contract

    The paginated list reads matches already persisted in MongoDB. Filtering and facets do not require waiting for Riot during that GET. Single-match detail adds timeline events; the list keeps participants and bans while avoiding that additional payload.

API & examples

Real endpoints, readable examples.

URLs use localhost and illustrative identities. These snippets describe the source-code contract; they are not production captures or performance measurements.

Example 01 / bash

Search without confusing a name and a region

The # character belongs to the query value and must be encoded. Omitting shard searches globally; EUW1 limits discovery to that shard. The response remains a list, including when no results are found.

curl --get 'http://localhost:8080/api/lol/search' \
  --data-urlencode 'q=Player#EUW' \
  --data-urlencode 'shard=EUW1'

Example 02 / http

Load a profile and distinguish its states

Game name and tag are separate path segments. An available response is a SummonerView without a data envelope. HTTP 202 means identity resolution or base loading is pending; HTTP 200 can already carry partial components.

GET /api/lol/EUW1/profile-by-name/Player/EUW?queue=RANKED_SOLO_5X5&season=2026

200 OK        → profile available, possibly partial
202 Accepted  → base profile still being prepared
404 Not Found → identity or profile not found

Example 03 / bash

Read one page of matches

Replace example-puuid with the PUUID returned by the profile. season=previous selects the configured previous season; limit and offset control pagination. lane filters the selected player, rather than any participant in the match.

curl --get 'http://localhost:8080/api/lol/EUW1/profile/example-puuid/matches' \
  --data-urlencode 'queue=RANKED_SOLO_5X5' \
  --data-urlencode 'lane=MID' \
  --data-urlencode 'season=previous' \
  --data-urlencode 'limit=20' \
  --data-urlencode 'offset=0' \
  --data-urlencode 'sort=timeStart:desc'

Timeline

Four years of evolution, not one magical rewrite.

Dates refer to changes recorded in Git. A commit or merge alone does not establish a production release date.

  1. The first Java bot

    The repository starts with the Java application and early Discord and audio features. Sound uploads and playback follow in March, establishing a capability that remains part of the project.

  2. From text commands to slash commands

    Slash-command work begins, followed by conversion of command families. Server administration, permissions and per-guild configuration evolve alongside it.

  3. League of Legends becomes a distinct capability

    The champion command and subsequent rune, profile and autocomplete improvements give the LoL integration structure. The bot is no longer only a set of audio commands.

  4. The first soundboard

    The history records a first soundboard release and public/private sound work. Community features keep evolving alongside game-related functionality.

  5. LP tracking

    LP tracking, the summoner track command and a dedicated graph arrive. Showing a current value is no longer enough: the system must preserve how it changes over time.

  6. LPTracker becomes MatchTracker

    The tracker is renamed because it now collects match data beyond LP. Bans, items and analysis follow, making persisted history the foundation of statistics.

  7. Statistics from the project’s own data

    The champion command starts using data collected by the system. Collection, persistence and analysis increasingly replace simply reading an external source.

  8. An HTTP interface for the LoL domain

    A dedicated branch introduces Spring work for League of Legends data. Services and contracts subsequently become a reusable boundary outside Discord.

  9. Redis enters the flow

    Redis caching is introduced, then extended across profiles, matches, builds and static data. Keys and expiration become explicit system responsibilities.

  10. Migrating LoL persistence to MongoDB

    The lol-api-mongo merge joins model, persistence, tracker and migration-documentation changes. LoL moves away from the old SQL paths; MariaDB retains other bot responsibilities.

  11. Background work becomes observable

    The job refactor reorganizes dispatchers, priorities and operational state. The current architecture separates the logical job registry from physical queues and workers.

  12. External caching for Riot payloads

    The history records R4J switching to EmptyCacheProvider and Redis owning Riot responses. Application cache, canonical storage and invalidation have clearer boundaries.

  13. Separation without pretending everything is distributed

    S4B artifact extraction and the lol-service alpha explore independent boundaries. They are separation milestones, not proof that every part is already an independently deployed service.

  14. History imports and job promotion

    A history endpoint accompanies MatchService and priority changes. Deduplication must still work when an interactive request arrives after a background import.

Current state

The current product is a shared platform.

The outcome is more than a bot with more commands. A Java core collects and stores matches, exposes HTTP contracts and reuses domain services inside Discord commands. Separating persistence, cache, computation and presentation lets Iodid exist without duplicating the backend.

Database size and latency depend on the dataset and environment. This reconstruction makes no new traffic or performance claims. Partial responses, rate limits and aggregate regeneration costs remain real constraints.

Design choices

The choices holding the system up.

Mongo is the source, Redis can be rebuilt

Matches, profiles and aggregates live in MongoDB. Redis holds rebuildable caches: profile components, Riot payloads, static catalogs and projections.

CostInvalidation isn’t automatic: every write has to handle it.

APIs don’t wait for Riot

A missing profile returns 202 and queues jobs. Partial data comes back as 200 with a refresh signal.

CostThe client has to handle PENDING, PARTIAL and READY states.

Priorities and deduplication before copies

Riot requests use per-shard queues. The job registry reuses active work and can promote its priority when an interactive request arrives.

CostLifecycle, priorities and child completion must remain consistent.

Scope

Included

  • Moderation and server management
  • Music, soundboards, playlists and TTS
  • Profiles, matches, builds and runes
  • Tracker, leaderboards and live games

In progress

  • Split into independent services
  • Riot workers and queues in dedicated modules
  • Shared repositories for jobs and DTOs

Out of scope

  • Public web interface: it lives in Iodid