Skip to content

← All projects

2016 → 2021 Self-employed · SDK & backend architect

Unitale — distributed Minecraft mini-games network, self-employed

Five years of solo dev: a custom Spigot fork, an 88K-line Java SDK, a type-safe mini-games framework, a 21K-line Symfony backend — operated for 100K+ unique players and 10K+ concurrent peak connections.

  • Java 8
  • Spigot (unispigot-server fork)
  • BungeeCord
  • Redisson
  • Hazelcast
  • Redis pub/sub
  • Symfony 3.3
  • Doctrine ORM
  • MariaDB
  • Predis
  • Maven / Nexus
  • Distributed systems

Context

Unitale was a Minecraft mini-games network I designed, built and operated solo from June 2016 to March 2021. Five years, 5,500 cumulative commits, 122,000 lines of Java and PHP across five interlocking repos: a server fork, a Bukkit SDK, a generic mini-games framework, two full mini-games, a stateless backend. In production: 100,000+ unique players, 10,000+ concurrent peak connections, 50+ Spigot instances continuously orchestrated.

The goal wasn’t to ship one more mini-game on a monolithic Mineplex/Hypixel-style cluster. It was to lay a distributed foundation — typed pub/sub, cross-server cache, clean lifecycle — that would let a single dev push a new mini-game in a few days, and let the infrastructure scale horizontally without rewriting the runtime each season.

My role

Solo full-stack architect and engineer: Java on the plugins and Spigot side, PHP on the backend, ops in production. Five years, one person, the whole chain — Minecraft protocol, distributed state, cross-game economy, monitoring, deployment.

What I built

The server fork — unispigot-server

Not a Spigot plugin. A custom build of the Minecraft 1.10.2 server, packaged as fr.unitale:unispigot-server:1.10.2-R0.1 and shipped through a private Nexus. The SDK and every game depend on it as scope=provided, which let me patch netcode and inventory rendering without waiting for an upstream Spigot release.

The SDK — 88,638 lines, 3,885 commits

The Java foundation everything else builds on. A few structuring pieces:

  • Typed Redis pub/sub bus: CommonPubSub and RequestPubSub expose enums (ChannelType, RequestType) where each value carries its own Consumer<String> dispatch handler. For cross-player requests (friends, parties, invites), the callback signature is curried — Function<UUID, Function<String, Function<RequestActionType, Function<UniPlayer, Consumer<String>>>>> — yielding a clean polymorphic async dispatcher without hand-rolled handler maps.
  • Dual-store distributed layer: Redisson for the transient and pub/sub layer, Hazelcast for shared caches (live matches, privacy codes, matchmaking history). The Hazelcast maps parties_dispatching, game_requests_history, privacy_codes are the cluster’s living memory.
  • 11+ structured API clients: SpigotPlayerRedisApiClient, SpigotServerRedisApiClient, FeatureRedisApiClient, PartyRedisApiClient, SettingsRedisApiClient, ModoRedisApiClient on the Redis side, paired with a REST family (PlayerRestApiClient, BoxRestApiClient, BoostRestApiClient…). Every domain knows how to fall back REST → Redis when either fails.
  • Direct NMS access rather than ProtocolLib. The fr.unitale.sdk.protocol.packets package contains ~30 UniPacketPlayOut* classes that intercept rendering server-side (custom inventories, player/NPC morphs, custom entities via EntityManager and CustomProjectileEntity). No external protocol dependency, no CPU overhead on the hot path.
  • Internationalization via multi-locale ResourceBundle (fr_FR, en, es) with twenty-some .properties files and a {{example.magic.key}} substitution system that keeps copy editable without touching code.

The mini-games framework — GameSDK2, 6,315 lines

Engine<I extends Instance<I, M, T>, M extends GameMap<I, M, T>, T extends UniTeam<I, M, T>> — an engine parameterized by three self-bound generics, forcing every mini-game to declare its own triplet of types and guaranteeing zero runtime casts. On top of it, a module system: Module<I, M, T> extends InstanceListener with a cascading lifecycle — WaitingModuleKitChoiceModule / KitViewModuleTeamModuleGameModuleDropModuleSpectateModule. Each module registers and unregisters cleanly on state transitions, isolating bugs in one module from corrupting the instance.

Player routing between matches relies on two room types: DispatchingRoom (the waiting area where matchmaking gets computed) and PassingRoom (inter-game transit). The QueueManager reads from a shared Hazelcast IMap, so any instance can reserve a slot on any other instance with no central coordinator.

The mini-games

  • LaserGame (1v1 / 2v2, 2,162 lines, 194 commits): a procedural matrix of laser weapons built from 7 lens types (FireballLens, GradientLens, NormalLens, SpiralLens, TriangleLens, WaveLens…) crossed with 7 explosion types (BurstExplosion, DropItemExplosion, LavaExplosion, NormalExplosion, SmokeExplosion, StarExplosion). Every lens × explosion combination yields a unique weapon, eliminating code duplication while delivering the variety expected from an arena FPS.
  • Arena (multi-team, 4,052 lines, 346 commits): 8 themed maps (Endare, Endare_hell, Wow, Pokeball, Mario, Hyperball, Volcan, Masterball), 30+ composable effects (CupidEffect, DuckEffect, FlameThrowerEffect, HammerEffect, MagicWandEffect, PokeballEffect, ToiletBrushEffect…) and 5 polymorphic listeners (BlockPlaceListener, BowShootListener, EntityDamageByPlayerListener, InteractListener, ProjectileHitListener) that dispatch each event to the relevant effects without a giant if/else.

The backend — Symfony rewrite, 21,087 lines, 515 commits

In 2020 I lifted persistence and economy out of the Java monolith into a Symfony 3.3 + Doctrine ORM + MariaDB + Predis backend, broken into 25 modular bundles: PlayerBundle, GameBundle, GameServerBundle, GuildBundle, ShopBundle, UserBundle, PermissionBundle, SanctionBundle, BoxBundle, FeaturesBundle, BoostBundle, SessionBundle, RedisBundle, etc. The contract with Spigot servers became REST by default, with a Redisson pub/sub fallback when REST timed out — a resilience pattern that saved many a peak-hour evening.

That backend runs the cross-server economy: gold (soft currency), emeralds (premium), cosmetics (auras, skins) unlocked through mini-game wins and persisted across every server in the network.

The hard part

Real-time cross-server state sync, in 2016, with no Kubernetes and no modern orchestrator. At the time the competition (Mineplex, Hypixel) ran on monolithic clusters with bespoke middleware; BungeeCord was just a TCP proxy. I chose to pair Redisson (transient, pub/sub) with Hazelcast (distributed maps, locks) to get both instant event broadcast and a coherent cache — the pattern you now find in modern Kafka + Redis stacks, but far from obvious on a Bukkit JVM in 2016.

The other, more structural hard part: keeping a generic mini-games framework healthy without a team. Self-bound nested generics like <I extends Instance<I, M, T>, M extends GameMap<I, M, T>, T extends UniTeam<I, M, T>> are expensive to design — they’re rare in the MC plugin ecosystem, where most networks inherit a monolithic GameEngine and cast aggressively. Holding that discipline solo for five years is what let LaserGame and Arena share 80% of their code without copy-paste. The curried functional signatures used for pub/sub dispatch go in the same direction: make the compiler prove consistency for me, because I had no QA behind me.

Results

  • 100K+ unique players, 10K+ concurrent peak connections, 50+ backend instances continuously orchestrated.
  • 5 years in production (June 2016 → March 2021), 5,500+ commits, 122,000 lines of Java + PHP, all maintained solo.
  • A mini-games framework reused across multiple internal games without copy-paste, validating the SDK-before-framework-before-games bet.
  • The longest-running proof in my career that a serious distributed architecture can be held by one person: pub/sub, coherent cache, clean lifecycle, REST/Redis resilience — the same skills that later carried me through Dofus Touch (live MMO backend), Authlete (multi-tenant OAuth SaaS), and today Conexia Tahiti (fractional CTO).