Skip to content

Service Container and Capabilities

frosxt edited this page Apr 14, 2026 · 1 revision

Service container and capabilities

PrisonCore has two ways for a module to find things at runtime: the service container, which is type-keyed and built into the kernel, and the capability registry, which is name-keyed and meant for cross-module discovery.

If you're trying to reach something the platform itself provides (the scheduler, the menu service, the storage registry, the message service), use the service container. If you're trying to find something another module published, use the capability registry.

ServiceContainer

com.github.frosxt.prisoncore.api.service.ServiceContainer

Resolved through your ModuleContext:

final TaskOrchestrator orchestrator = context.services().resolve(TaskOrchestrator.class);

Methods:

<T> T resolve(Class<T> type);
<T> Optional<T> resolveOptional(Class<T> type);
<T> Optional<T> resolveByName(String name, Class<T> type);
boolean has(Class<?> type);
<T> Provider<T> provider(Class<T> type);
<T> void register(ServiceDescriptor<T> descriptor);
Scope createModuleScope(ModuleHandle moduleHandle);

resolve(Class) is the one you'll use 99% of the time. It throws if the service isn't registered.

resolveOptional(Class) returns an Optional for services that may not be present (for example, services another module publishes that yours optionally integrates with).

resolveByName(String, Class) is for the rare case where two services share an interface and need to be told apart by name.

provider(Class) returns a lazy Provider<T> for services with cyclical or deferred wiring.

register(ServiceDescriptor) adds a new service to the container. Most modules don't need this; you only register services here when you want other modules to resolve something you provide. See ServiceDescriptor below.

Services the kernel always exposes

These are registered before any POST_INFRASTRUCTURE module loads. You can resolve them in your onPrepare.

  • TaskOrchestrator — scheduling. See Scheduler.
  • CommandService — register commands. See Commands.
  • MenuService — open inventory menus. See Menus.
  • MessageService — send keyed multi-channel messages. See Messages and Placeholders.
  • MessageCatalog — the message template store backing MessageService.
  • PlaceholderService — register placeholder resolvers and process templates.
  • StorageRegistry — acquire backend handles for JSON/SQLite/SQL/Mongo. See Storage.
  • ConfigService — generic typed YAML loader for files outside core.yml.
  • CoreConfig — the parsed core.yml. Read this for storage.backend.
  • DomainEventBus — cross-module domain event publishing and subscription.
  • BukkitListenerHost — register raw Bukkit listeners through the platform. See Events and Listeners.
  • PlayerProfileService — player profile load/save and session management.
  • PlatformInfo — platform metadata (version, server type).
  • ObservabilityService — kernel-managed metrics and timings.
  • CapabilityRegistry — also reachable via context.capabilities().

ServiceDescriptor

If you need to publish your own service into the container so other modules can resolve it, build one with ServiceDescriptor.builder(Class).

context.services().register(
    ServiceDescriptor.builder(MyService.class)
        .instance(new MyServiceImpl())
        .scope(ServiceScope.KERNEL)
        .build()
);

Builder methods:

  • factory(Function<ServiceContainer, T>) — lazy construction. The function runs when the first resolve lands. Use this when your service depends on another service in the container.
  • instance(T) — direct instance for things that are already constructed.
  • scope(ServiceScope)KERNEL, MODULE, or SESSION. Defaults to KERNEL.
  • dependsOn(Class<?>... types) — declare dependencies for the container's topological initialization.
  • name(String) — give the descriptor a name so callers can reach it via resolveByName.
  • onInitialize(Consumer<T>) — a callback that fires once after construction.
  • onDestroy(Consumer<T>) — a callback that fires when the container shuts the service down. Use this if your service holds resources that need closing.

For most external modules, registering a service in the kernel container is overkill. Publish a capability instead.

ServiceScope

com.github.frosxt.prisoncore.api.service.ServiceScope

public enum ServiceScope { 
    KERNEL, 
    MODULE, 
    SESSION 
}

KERNEL lives for the whole platform lifetime and is shared across modules. The default.

MODULE lives for a single module's lifetime. Created when the module enables, disposed when it disables. Use this when each loaded module needs its own instance of a service.

SESSION lives for a single player session. Created on join, disposed on quit (or kernel shutdown).

CapabilityRegistry

com.github.frosxt.prisoncore.api.capability.CapabilityRegistry

Reached via context.capabilities(). This is how modules find each other without having to know each other's concrete implementation classes.

<T> void register(CapabilityKey<T> key, T implementation);
<T> T resolve(CapabilityKey<T> key);
<T> Optional<T> resolveOptional(CapabilityKey<T> key);
<T> void registerIfAbsent(CapabilityKey<T> key, T implementation);
boolean has(CapabilityKey<?> key);
Set<CapabilityKey<?>> allKeys();
int size();

void registerMarker(String qualifiedName);
boolean hasMarker(String qualifiedName);
Set<String> allMarkers();

register publishes an implementation for a key. It throws on duplicate registration, which is the right behavior — two modules publishing the same capability is a configuration error.

resolve throws if the key isn't published. resolveOptional returns empty.

registerIfAbsent is the soft variant of register for capabilities multiple modules might race to publish.

The Marker family is for presence-only capabilities where there is no concrete implementation to hand out. You're saying "this feature exists, do whatever you do when it does."

CapabilityKey

com.github.frosxt.prisoncore.api.capability.CapabilityKey<T>

public static <T> CapabilityKey<T> of(String namespace, String name, Class<T> contractType);
public static <T> CapabilityKey<T> of(String name, Class<T> contractType); // namespace defaults to "core"

public String namespace();
public String name();
public Class<T> contractType();
public String qualifiedName(); // "namespace:name"

The namespace is the module id by convention. The name is the capability you're advertising. The contract type is the interface consumers will cast to.

private static final CapabilityKey<EconomyService> ECONOMY =
    CapabilityKey.of("economy", "economy-service", EconomyService.class);

// publishing
context.capabilities().register(ECONOMY, myEconomyImpl);

// consuming
final EconomyService economy = context.capabilities().resolve(ECONOMY);

Two keys are equal when their namespace and name match. The contract type is type-safety scaffolding and does not participate in equality.

When to use which

If the platform itself provides it (or your module is wiring something into the kernel for everyone), use ServiceContainer.

If your module exposes a feature for other modules to optionally integrate with, publish a CapabilityKey. Other modules can call resolveOptional to check, and your module is allowed to be absent without breaking theirs.

If you only need to know whether a feature exists, use a marker capability and skip the typed handle entirely.

Clone this wiki locally