Architecture Overview¶
Extended version of ARCHITECTURE.md. This document goes package by package through what exists and why.
The Layers in Code¶
| Layer | Package(s) | Example |
|---|---|---|
| Frontend | gui, command (later: web) |
UniversalAdminCommand |
| Application Services | module-internal, e.g. modules.players.PlayerService |
PlayerService |
| Actions / Domain Logic | action, module-internal modules.*.action |
GetPlayerProfileAction |
| Repositories | storage (interface), storage.jdbc/modules.*.jdbc (adapter) |
JdbcPlayerProfileRepository |
| Paper / Database | Bukkit API, javax.sql.DataSource |
- |
Each row only knows the row below it through an interface, never a concrete
implementation two rows down. A GuiPage knows a Service, never a
Repository. A Service knows a Repository interface, never
Connection.
The Composition Root: dev.universaladmin.core¶
UniversalAdmin- holds every shared registry/shared service (plugin instance, version, start time,SettingRegistry+SettingsService, scheduler, storage, actions, GUI pages,GuiFramework(sessions/icons - see docs/development/gui-framework.md), permissions,ModuleRegistry, audit, messages, notifications, a genericServiceRegistryfor services shared across modules). Built once inUniversalAdminPlugin#bootstrapCoreand passed to every module viaModuleContext.PluginStatus/ComponentStatus-UniversalAdmin.status()builds a fresh snapshot on every call (version, uptime, active/failed modules fromModuleRegistry, coarse database/web status). Nothing is cached - two calls a minute apart can show different module lists. Used by the/admincommand.ServiceRegistry- a type-based registry (Class<T> → T) for services more than one module needs (example:PlayerService, whichPlayersModuleregisters and which e.g.ModerationModulecan later look up without building its own copy).registry.Registry/registry.SimpleRegistry- the generic, thread-safe registry implementationActionRegistry,GuiRegistry, andPermissionRegistrybuild on.id.Key- anamespace:nameidentifier, the basis forModuleId,ActionId,GuiPageId,AuditEventType. The namespace is the collision protection for future extensions (core:playersvs.myext:players).
module - the Lifecycle System¶
Module is the interface every built-in module (and later every
extension) implements: a ModuleDescriptor (id, name, description,
dependencies, settings/permission namespace, optional GUI icon), plus
onLoad/onEnable/onDisable. ModuleRegistry tracks each module's state
(DISCOVERED/LOADED/ENABLED/DISABLED/FAILED); ModuleManager
drives the transitions in dependency order (topological sort over
ModuleDescriptor.dependencies()) and isolates a failing module (FAILED,
fully logged) instead of aborting the whole server start.
ModuleResources (part of ModuleContext) automatically releases
listeners/tasks/registry entries a module registers in onEnable again on
disable. Full detail: modules.md.
action - Where Business Logic Lives¶
Action<I, R> is a single operation callable from any frontend.
ActionResult<R> is a sealed success/failure type instead of exceptions or
null. Actor/ActorType describe who is acting (player, console,
system, future: web) without action depending on Bukkit types. Detail:
actions.md.
gui and command - the Frontends¶
GuiPage is the only place allowed to touch Bukkit inventory APIs.
UniversalAdminCommand is the root command (/admin, aliases /ua,
/uadmin) - currently a status command that renders
UniversalAdmin.status(), not a feature command. It's the one documented
exception to "a frontend only gets the one dependency it needs": a root
status command legitimately needs read access to the whole platform. Every
later GuiPage, by contrast, gets exactly the services/actions it needs
through the constructor, not through runtime access to a global object.
Detail: gui.md.
storage - Persistence¶
Repository<T, ID> is the standard shape for data access, always async
(CompletableFuture). Migration/MigrationRunner version the schema.
storage.jdbc.DataSourceFactory is the only place that builds
HikariConfig and JDBC URLs. Detail: storage.md.
permission, audit, settings, config, localization, notification, scheduler¶
One focused cross-cutting service each:
permission-PermissionNode/PermissionDefinition/PermissionRegistry, testable independent of Paper; syncing to Bukkit'sPluginManageronly happens in bootstrap.audit-AuditService, JDBC implementation inaudit.jdbc. Every action with a visible effect should write an entry here on success.settings- the typed settings system:SettingKey/SettingType/SettingDefinition/SettingValidator/SettingRegistry/SettingsService,CoreSettings(every core setting),YamlSettingsServiceas the Paper adapter,ReloadConfigActionfor/admin reload. Fully replaces scatteredconfig.getString(...)calls. Detail: ../development/settings.md.config- deliberately small: onlyConfigMigration/ConfigMigrationRunnerforconfig.ymlversioning (config-version), analogous tostorage.Migrationfor the database.localization-MessageService/MessageKey(Paper-independent, returns plain strings),YamlLocaleMessageServiceas the multi-language adapter overlang/*.ymlwith a fallback chain (active locale →en_US→ a visible marker),ComponentMessagesas a thin MiniMessage renderer for in-game output.notification-NotificationService, currently only in-game chat (InGameNotificationService); the interface is already cut so a Discord/web channel only has to implement it.scheduler-TaskScheduler/PaperTaskScheduler, see threading.md.
modules.* - the Built-in Modules¶
See modules.md for the full list and
adding-module.md for the guide to
building a new one. modules.players is the reference implementation.
Why One Gradle Project Instead of Four¶
universaladmin-api, -sdk, -web would today only consist of empty
folders - there are no external extensions and no web app yet that would
compile against them. The separation point is instead marked in code (see
extensions.md and
web-future.md) and becomes a real module split once
something exists that needs it. Detail:
decisions/0006-optional-web-architecture.md.