Architecture

This page explains how LeonsDB is built and why. Sections marked (planned) describe the design for parts that are not built yet; they are written down now so that early decisions don’t block them later.

The pieces

 Minecraft servers (Paper)                      LeonsDB server (Go)
 ┌──────────────────────────┐   gRPC / TLS    ┌──────────────────────────┐
 │ your plugin              │◄───────────────►│ API layer (gRPC)         │
 │   └ LeonsDB API          │  token per      │   └ engine: rules, ops,  │
 │ LeonsDB connector        │  server         │       leases, indexes    │
 │   ├ local player cache   │                 │       └ storage: Pebble  │
 │   └ write merger         │                 │           (WAL + LSM)    │
 └──────────────────────────┘                 └──────────────────────────┘

Data model

Data is organized as namespace → collection → record. A namespace usually belongs to one plugin (practicepvp), and a collection holds one kind of data (stats).

Names are 1–64 characters, start with a lowercase letter and contain only lowercase letters, digits and underscores. The connector checks this at startup, and from Phase 1 the server enforces it too.

Records have typed fields: int, long, double, bool, string, UUID, timestamp, list and map, plus Minecraft types (items, inventories, locations).

Items are stored in Minecraft’s own format. Since Minecraft 1.20.5 the item format (“data components”) changes in almost every release. Instead of inventing an encoding that would break on every update, LeonsDB stores the bytes from Paper’s ItemStack.serializeAsBytes() together with the Minecraft data version. When an old item is loaded on a newer server, Paper upgrades it with Mojang’s own data fixers. It is compact binary, not JSON.

Updates are operations, not overwrites: set, increment, append, remove. The connector merges operations on the same field before sending them (40 × “kills +1” becomes “kills +40”), and the server applies all operations of a request atomically.

Storage layout (Phase 1)

All data lives in one Pebble database inside the data directory. Keys are binary and order-preserving, so related data sits together on disk and range scans are cheap:

[key type][namespace id][collection id][record key]

Namespace and collection names are mapped to short numeric IDs, keeping keys small. Index entries (Phase 4) use the same scheme, so Pebble’s sort order gives sorted indexes for free.

Player ownership and fencing (Phase 3)

Only one Minecraft server may write a player’s data at a time. A server acquires a lease when the player joins, renews it with heartbeats, and releases it when the player leaves. If a server crashes, its leases expire.

Leases alone are not enough. A server can freeze (a long garbage collection pause, a lag spike) for longer than its lease, wake up, and write stale data after another server has already taken over. To stop that, every lease acquisition returns an epoch, a number that only ever increases. Every write carries the epoch it was made under, and the server rejects writes with an epoch older than the current one. A frozen server’s late writes are refused, no matter how late they arrive.

The epoch is part of the write path from Phase 1 on, so adding leases does not change the protocol.

Fields that must never be lost (Phase 2)

The connector batches normal writes and sends them about once per second. Fields marked @Transactional, typically money, skip that buffer: each change goes to the server immediately (still without blocking the main thread) and only counts as done once the server has stored it durably.

Sorted indexes (Phase 4)

A numeric field marked @Indexed is kept in a sorted index on every write. Top-N queries read the index in order. “Rank of player X” needs to count the entries above a value, which an LSM store like Pebble cannot do quickly. LeonsDB keeps a counting skip list in memory per index, which answers rank queries in O(log n), and rebuilds it from the on-disk index on startup.

Security defaults