Skip to content

Module Lifecycle

frosxt edited this page Apr 14, 2026 · 1 revision

Module lifecycle

Every module the kernel loads goes through three hooks in a fixed order: prepare, enable, disable. The order is enforced. You cannot skip a phase, and the kernel will not call enable on a module whose prepare threw.

The hooks

AbstractPlatformModule exposes three abstract methods. You implement these.

protected abstract void onPrepare(ModuleContext context);
protected abstract void onEnable(ModuleContext context);
protected abstract void onDisable(ModuleContext context);

onPrepare runs first, before any other module's onEnable. This is where you build services, open files, and resolve everything you need from the kernel container. You should not register commands, open menus, or assume other modules' capabilities are visible yet.

onEnable runs after every module in your load phase has been prepared. By this point all the platform's services are live and other modules' capabilities are queryable. Register commands, listeners, and capabilities here.

onDisable runs in reverse load order during shutdown. Cancel scheduled tasks. Unregister listeners and commands. Flush any persistent state. Close resources you opened.

Load phases

ModuleLoadPhase controls when your module loads relative to kernel infrastructure. Set it on @ModuleDefinition with loadPhase = ....

PRE_INFRASTRUCTURE — your module loads during the kernel's bootstrapping phase, before infrastructure services are finalized. Use this only when you contribute infrastructure that other modules depend on. The canonical example would be registering a custom StorageBackendFactory. Most modules should not pick this.

POST_INFRASTRUCTURE — the default. Your module loads once the kernel reports INFRASTRUCTURE_READY. Storage, scheduler, command service, menu service, message service, placeholder service, and the player profile service are all available. This is where the vast majority of modules live.

LATE — your module loads after every POST_INFRASTRUCTURE module has been enabled. Useful when you depend on capabilities that another POST_INFRASTRUCTURE module publishes during its own onEnable. If you can express the dependency through requiredDependencies instead, prefer that.

ModuleContext

The single argument to all three hooks. It exposes everything you need to wire up your module.

public interface ModuleContext {
    ModuleDescriptor descriptor();
    ServiceContainer services();
    CapabilityRegistry capabilities();
    Logger logger();
    Path dataFolder();
}

descriptor() returns the resolved ModuleDescriptor for your module — the one the kernel built from your @ModuleDefinition.

services() is the kernel's service container. This is how you reach TaskOrchestrator, MenuService, MessageService, StorageRegistry, and the rest. See Service Container and Capabilities.

capabilities() is the capability registry. Publish things here for other modules to discover, or look up things other modules published.

logger() is a java.util.logging.Logger prefixed with your module id. Use it instead of System.out or Bukkit.getLogger().

dataFolder() is the path to your module's dedicated data directory. The kernel creates the directory before onPrepare runs, so you can write to it immediately.

What you can rely on in each hook

In onPrepare:

  • dataFolder() exists.
  • services().resolve(...) works for any kernel service registered before your load phase. For POST_INFRASTRUCTURE modules that means everything.
  • Other modules in the same load phase may not be enabled yet, so don't query their capabilities.

In onEnable:

  • Every module in your load phase has been prepared. The platform considers itself live.
  • Capabilities published by other modules in the same phase during their own onPrepare are visible.
  • It is safe to register commands, open menus, and start scheduled tasks.

In onDisable:

  • The kernel is shutting down.
  • The order is the reverse of how modules were enabled, so modules you depend on are still running.
  • Treat every field as possibly null. If onPrepare threw on line 5, fields after line 5 will be null when onDisable runs.

A complete example

public final class ExampleModule extends AbstractPlatformModule {

    private TaskHandle pollTask;

    @Override
    protected void onPrepare(final ModuleContext context) {
        // Build internal services here. Don't touch the world yet.
    }

    @Override
    protected void onEnable(final ModuleContext context) {
        final TaskOrchestrator orchestrator = context.services().resolve(TaskOrchestrator.class);
        final TaskSpec spec = TaskSpec.builder(this::poll)
                .delay(Duration.ofSeconds(5))
                .period(Duration.ofSeconds(30))
                .build();
        pollTask = orchestrator.io(spec);
    }

    @Override
    protected void onDisable(final ModuleContext context) {
        if (pollTask != null) {
            pollTask.cancel();
            pollTask = null;
        }
    }

    private void poll() {
        // ...
    }
}

Lifecycle invariants worth knowing

The state machine is forward-only. The kernel transitions through a fixed enum sequence (CREATED, BOOTSTRAPPING, INFRASTRUCTURE_READY, MODULES_DISCOVERED, MODULES_RESOLVED, MODULES_PREPARED, MODULES_ENABLED, ACTIVE, QUIESCING, DISABLED). Modules cannot induce a backwards transition.

If onPrepare throws, the module is marked failed and onEnable will not be called. onDisable is still called for cleanup.

The kernel shuts down modules in reverse enable order. If you depend on another module, it is guaranteed to still be enabled when your onDisable runs.

Clone this wiki locally