The complete ecosystem: from a live game to a build-optimization web app
Helping a player compare two equipment builds assumes a prerequisite that's easy to forget once you're looking at the finished interface: you need clean, complete, up-to-date game data. Not just item stats, but their translations, their sprites, their relationships to one another, resynced with every patch the game ships.
That prerequisite alone ended up shaping five distinct systems before a single line of the comparison interface was written. This article is the grand tour: how these five building blocks fit together, and why they stayed separate instead of being merged into one project.
Before diving into the details of each block, a simplified diagram beats a long paragraph. Here's the pipeline at a glance, from the game client to the browser:
Block 1: decoding what the game doesn't document
It all starts with an acquisition problem. A live MMO's client stores its data in positional binary files, with no tags or field names, and this format drifts slightly with every patch. Before anything can be shown on the product side, you first need a tool able to track that drift from version to version, without rewriting the decoder by hand on every update. The same problem shows up for the game's tooltips, which aren't plain text but a small DSL with its own references and its own formatting logic. This block stays deliberately the most closed off of the five: it's the one that touches the game client directly.
Block 2: aggregating, loading, and separating what's public
Once the data is extracted, it still needs to be made usable. The ETL hub takes the raw output from the previous block, cross-references it, enriches it, and publishes it in two forms: versioned JSON artifacts on one side, a populated Postgres database on the other. This is also the layer where a distinction plays out that runs through the rest of the pipeline: some data is public (the data that ends up in the product), some stays internal (the data that only serves the pipeline itself). This hub's code is organized around a fairly strict hexagonal architecture, so that the input adapters, the raw formats that change with every patch, stay decoupled from the core business logic.
Block 3: rendering a sprite with no graphics card
One of the artifacts produced by the ETL hub is the characters' animation files and texture atlases. The sprite rendering service, written in Rust, takes these artifacts and composes a PNG image on demand, server-side, with no GPU available. The constraint isn't producing a nice-looking image but producing the exact same image the game client would display, pixel for pixel. This service runs apart from the others because its workload, CPU-intensive rasterization, and its lifecycle have nothing in common with those of a typical web server.
Block 4: the product the player sees
This is the visible block: a web app with Next.js on the front end and AdonisJS for the API, letting a player build a piece of equipment, compare it to others, and share it. It consumes the Postgres database populated by the ETL hub, queries the rendering service to display sprites, and relies on Redis to cache the most frequent reads. Two pieces deserve their own dedicated article: how the database resyncs with a new export without ever taking the service down, and searching for items by visual hue rather than by name, built on a color distance in Lab space.
Block 5: serving images without reinventing a CDN
Last block, the most modest in code size but not in usefulness: a small service that serves images (item icons, rendered sprites, illustrations) with resizing and caching, deliberately stopping short before adding features a real CDN already handles better. That choice of scope is what keeps this service tenable at ninety lines rather than a few thousand.
The thread that ties the five together
What makes these five repositories cohere isn't a shared deployment or a shared language (there's decompiled Java, Rust, TypeScript, SQL), it's a version number that propagates in a single direction.
- 1
A patch ships
The game releases a patch, which triggers block 1. - 2
Versioned decoding
Block 1 produces new versioned artifacts. - 3
Aggregation
The ETL hub consumes them and publishes its own aggregate version. - 4
Consumption
Rendering and the web app each pull the artifact version that concerns them. - 5
Reseed
Down to reseeding the product database.
At no point does a downstream block guess at or recompute a piece of data that an upstream block should have already handed it fully resolved.
What these five blocks taught me
The temptation, early on, would have been to put everything into a single monolithic repository: after all, it's the same project. But each block has a completely different rate of change, language, and load profile. The binary decoder only runs when a patch ships. The rendering service runs continuously but needs very little persistent memory. The web app, meanwhile, moves at the pace of a product with live users. Forcing them into the same deployment would have coupled their lifecycles for no good reason.
Note
Separating these blocks forced a useful discipline: every boundary between two repositories is an explicit interface, with a versioned data format, that can't be worked around by quietly importing an internal function from another module.
This discipline, applied to a context where you control neither the source format nor its rate of change, is the thread running through everything else.