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
API
Redis
MongoDB
Riot
GET/api/lol/EUW1/profile/{puuid}
REDISGET beebot:lol:ls:EUROPE:EUW1:{puuid}:summonerHIT · 2 ms
200SummonerView6 ms
API
Redis
MongoDB
Riot
GET/api/lol/EUW1/profile/{puuid}
REDISGET beebot:lol:ls:EUROPE:EUW1:{puuid}:summonerMISS · 1 ms
MONGOsummoner.findOne({ puuid })HIT · 9 ms
REDISSET beebot:lol:ls:EUROPE:EUW1:{puuid}:summoner1 ms
200SummonerView15 ms
API
Redis
MongoDB
Riot
GET/api/lol/EUW1/profile/{puuid}
REDISGET beebot:lol:ls:EUROPE:EUW1:{puuid}:summonerMISS · 1 ms
RIOTGET /riot/account/v1/accounts/by-puuid/{puuid}200 · 141 ms
RIOTGET /lol/summoner/v4/summoners/by-puuid/{puuid}200 · 152 ms
MONGOsummoner.insertOne(profile)7 ms
REDISSET beebot:lol:ls:EUROPE:EUW1:{puuid}:summoner1 ms
200SummonerViewRETRY · 6 ms
API
Redis
MongoDB
Riot
GET/api/lol/EUW1/profile-by-name/Player/EUW
REDISRiot ID lookup · Player#EUW · EUW1MISS · 1 ms
MONGOsaved summoner lookup by Riot IDMISS · 8 ms
RIOTGET /riot/account/v1/accounts/by-riot-id/Player/EUW404 · 138 ms
404not_found149 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.
01
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.
02
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.
03
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.
04
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.
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.
Dates refer to changes recorded in Git. A commit or merge alone does not establish a production release date.
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.
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.
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.
The first soundboard
The history records a first soundboard release and public/private sound work. Community features keep evolving alongside game-related functionality.
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.
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.
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.
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.
Redis enters the flow
Redis caching is introduced, then extended across profiles, matches, builds and static data. Keys and expiration become explicit system responsibilities.
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.
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.
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.
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.
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.