Architecture
XDiscordUltimate is a modular Bukkit plugin that bridges a Minecraft server and a Discord bot. A small core orchestrates aModuleManager, a JDA-backed DiscordManager, an async DatabaseManager, and a reflection-based event bus. Every feature is implemented as a Module.
System Architecture
The main classXDiscordUltimate extends JavaPlugin owns every subsystem as a field and exposes it through getters. It is a singleton: XDiscordUltimate.getInstance().
Lifecycle
Enable
Order matters: theLibraryManager (Libby) must finish before anything that touches JDA or JDBC, because those libraries are downloaded at runtime rather than shaded. If loadAllLibraries() throws, the plugin disables itself. The same is true for DatabaseManager.initialize() — a SQL failure disables the plugin.
Disable
onDisable() reverses the order: detach the ConsoleAppender, let the chat-bridge and server-logging modules send their shutdown messages, ModuleManager.disableModules(), DiscordManager.shutdown(), then DatabaseManager.close().
Module System
Every feature is aModule. The ModuleManager registers the 19 built-in modules in loadModules(), resolves their @DependsOn dependencies, checks each module’s feature flag, and calls enable(). The abstract base class defines the contract:
enable() and disable() are the public entry points — they guard onEnable()/onDisable() with try/catch and flip the enabled flag. Modules must implement only the four abstract members; everything else has a no-op default.
Configuration
getConfig() resolves the module’s features.<name> ConfigurationSection, where <name> is getName() lowercased with spaces replaced by hyphens. A module named "Chat Bridge" reads from features.chat-bridge. The feature flag itself is ConfigManager.isFeatureEnabled(key) — the key matches the registration key in ModuleManager.loadModules() (for example "chat-bridge").
Dependencies
A module may declare required modules with@DependsOn. ModuleManager.checkDependencies() reads the annotation reflectively and refuses to enable the module unless every dependency is enabled in config.
ModuleManager API
| Method | Returns |
|---|---|
getModule(String key) | Module by registration key, or null |
<T extends Module> getModule(Class<T>) | module of the given class, or null |
getModules() | defensive copy Map<String, Module> |
isModuleActive(String key) | true if loaded and enabled |
reloadModules() | full restart — disable all, clear map, reload |
reloadModuleConfigs() | hot reload cached config in each enabled module |
onDiscordReady() | fan-out to every enabled module |
Data Layer
DatabaseManager wraps a HikariCP HikariDataSource. SQLite is the default; MySQL and PostgreSQL are selected by database.type in config.yml. Pool sizing is fixed: max 10 connections, min idle 2, 30 s connection timeout, 60 s leak detection.
Async by Default
Every read and write goes throughexecuteAsync, which runs a DatabaseOperation<T> on the common ForkJoinPool:
SQLException inside the operation is logged and rethrown wrapped in a RuntimeException, so the CompletableFuture completes exceptionally. Typical call sites:
getDiscordIdSync(UUID), getMinecraftUuidSync(String), isDiscordLinkedSync(String), and getLinkedAccountInfoSync(String) (returns a LinkedAccountInfo snapshot). Never call the sync variants on the main server thread.
Schema and Migrations
Tables are created idempotently oninitialize():
verified_users, verification_codes, tickets, ticket_messages, moderation_logs, player_stats, activity_rewards, schema_versions.
Versioning is tracked in schema_versions. runMigrations() compares the stored version against CURRENT_SCHEMA_VERSION and applies each missing migration in order. Each migration step updates the stored version only after its statements succeed.
Inner Data Classes
LinkedAccountInfo— immutable snapshot withgetDiscordId(),getDiscordName(),getMinecraftName(),getMinecraftUuid(),getLinkedAt().VerificationCode—getDiscordId(),getCode(),getUsername(),getCreatedAt(),getExpiresAt(),isExpired().
Event Bus
XDiscordEventBus is a static, reflection-based dispatcher. It is not a Bukkit event bus, and events are plain POJOs — do not annotate handlers with @EventHandler and do not implement Listener.
publish(event) the bus iterates every registered listener and, for each declared method with exactly one parameter whose type isInstance(event), calls setAccessible(true) and invoke(). That means a listener can name its handler method anything and can handle multiple event types by overloading the parameter. Exceptions inside a handler are swallowed so one failing listener cannot break dispatch.
Example event:
PlayerUnlinkedEvent follows the same shape. The core publishes these after the database write for linking/unlinking succeeds.
Technology Stack
Platform
Spigot/Paper 1.16.5+ API, Java 16 source/target, Java 17+ runtime.
Discord
JDA 5.0.0-beta.18 (fetched at runtime by Libby).
Persistence
HikariCP 5.0.1 pool, SQLite/MySQL/PostgreSQL JDBC drivers (runtime-fetched).
Optional Hooks
LuckPerms 5.4, PlaceholderAPI 2.11.3, Vault 1.7 (all compileOnly).
Next Steps
Writing Modules
Implement a feature on the
Module abstraction.API Overview
Database, event, and manager APIs in detail.

