Skip to content

Dependency Injection for PaperMC Plugins

Reedwork provides dependency injection for PaperMC plugins, allowing Java components to receive their dependencies automatically through constructor injection.

Instead of manually creating services and passing them between commands, event listeners, and other plugin components, you declare what a component needs and Reedwork resolves the dependency graph when the component is created.

This keeps your Paper plugin architecture modular while keeping dependencies explicit in ordinary Java constructors.

The primary way to use dependency injection in Reedwork is constructor injection.

A component declares the services it requires in its constructor:

ReedworkCommand.java
package dev.reedworkmc.examples.dependencyinjection.commands;
import dev.reedworkmc.examples.dependencyinjection.services.ExecuteCounter;
import dev.reedworkmc.examples.dependencyinjection.services.GreeterService;
import dev.reedworkmc.examples.externallib.FileFromExternalLib;
import dev.reedworkmc.reedwork.annotation.Command;
import dev.reedworkmc.reedwork.annotation.CommandHandler;
import dev.reedworkmc.reedwork.annotation.SubCommand;
import dev.reedworkmc.reedwork.command.CommandContext;
import net.kyori.adventure.text.Component;
@Command("reedwork")
public final class ReedworkCommand {
private final GreeterService greeterService;
private final ExecuteCounter executeCounter;
private final FileFromExternalLib fileFromExternalLib;
public ReedworkCommand(GreeterService greeterService, ExecuteCounter executeCounter, FileFromExternalLib fileFromExternalLib) {
this.greeterService = greeterService;
this.executeCounter = executeCounter;
this.fileFromExternalLib = fileFromExternalLib;
}
@CommandHandler
public boolean execute(CommandContext context) {
greeterService.sendGreeting(context.player());
executeCounter.increment();
return true;
}
@SubCommand("count")
public boolean count(CommandContext context) {
context.player().sendMessage(Component.text("Reedwork Command fired " + executeCounter.getExecutionCount() + " times!"));
return true;
}
@SubCommand("amount set <amount>")
public boolean setAmountToAdd(CommandContext context, Integer amountToAdd) {
fileFromExternalLib.setAmountToAdd(amountToAdd);
return true;
}
@SubCommand("amount get")
public boolean getAmountToAdd(CommandContext context) {
context.player().sendMessage(Component.text("Amount to Add: " + fileFromExternalLib.getAmountToAdd()));
return true;
}
}

In this example, Reedwork resolves GreeterService, ExecuteCounter, and FileFromExternalLib when it creates ReedworkCommand.

The command does not need to create those objects itself or retrieve them from a service locator.

The constructor documents the dependencies directly:

ReedworkCommand
├── GreeterService
├── ExecuteCounter
└── FileFromExternalLib

This makes dependencies visible in the Java type structure of your Paper plugin.

Reedwork supports different lifecycles for dependency-injected components.

The two component annotations are:

  • @Singleton
  • @Transient

The lifecycle determines how Reedwork creates and reuses component instances.

A @Singleton component is created once and the same instance is reused whenever that component is injected.

For example:

ExecuteCounter.java
package dev.reedworkmc.examples.dependencyinjection.services;
import dev.reedworkmc.examples.externallib.FileFromExternalLib;
import dev.reedworkmc.reedwork.annotation.Singleton;
@Singleton
public final class ExecuteCounter {
private int executionCount;
private final FileFromExternalLib fileFromExternalLib;
public ExecuteCounter(FileFromExternalLib fileFromExternalLib) {
this.fileFromExternalLib = fileFromExternalLib;
}
public void increment() {
executionCount += fileFromExternalLib.getAmountToAdd();
}
public int getExecutionCount() {
return executionCount;
}
}

ExecuteCounter can therefore maintain state across multiple command executions.

Its constructor can also declare dependencies:

ExecuteCounter.java
package dev.reedworkmc.examples.dependencyinjection.services;
import dev.reedworkmc.examples.externallib.FileFromExternalLib;
import dev.reedworkmc.reedwork.annotation.Singleton;
@Singleton
public final class ExecuteCounter {
private int executionCount;
private final FileFromExternalLib fileFromExternalLib;
public ExecuteCounter(FileFromExternalLib fileFromExternalLib) {
this.fileFromExternalLib = fileFromExternalLib;
}
public void increment() {
executionCount += fileFromExternalLib.getAmountToAdd();
}
public int getExecutionCount() {
return executionCount;
}
}

Reedwork resolves those dependencies as part of creating the singleton component.

Use a singleton when the same component instance should be shared across the parts of your plugin that depend on it.

A @Transient component creates a new instance whenever Reedwork resolves it.

For example:

GreeterService.java
package dev.reedworkmc.examples.dependencyinjection.services;
import dev.reedworkmc.reedwork.annotation.Transient;
import net.kyori.adventure.text.Component;
import org.bukkit.entity.Player;
@Transient
public final class GreeterService {
public void sendGreeting(Player target) {
target.sendMessage(Component.text("Welcome to Reedwork!"));
}
}

Transient components are useful when each resolution should receive a separate instance rather than sharing one object.

The lifecycle is part of the dependency configuration, so you can choose whether a component should be shared or recreated.

Not every dependency needs to be a Reedwork component.

You can register external classes manually through the Reedwork builder.

For example, an external library might provide a class that cannot be annotated with Reedwork:

public final class FileFromExternalLib {
private int amountToAdd;
public FileFromExternalLib(int amountToAdd) {
this.amountToAdd = amountToAdd;
}
public int getAmountToAdd() {
return amountToAdd;
}
public void setAmountToAdd(int amountToAdd) {
this.amountToAdd = amountToAdd;
}
}

The class does not need a Reedwork annotation.

Instead, you can explicitly bind an instance:

Reedwork.create(this)
.bind(FileFromExternalLib.class)
.toSingleton(new FileFromExternalLib(42))
.scan("dev.reedworkmc.examples.dependencyinjection");

The supplied instance is registered as a singleton.

Whenever FileFromExternalLib is injected through this binding, Reedwork provides the registered instance.

This is useful for dependencies that come from external libraries or objects that need to be configured before they enter the dependency graph.

External classes can also be registered as transient dependencies.

Reedwork.create(this)
.bind(FileFromExternalLib.class)
.to(FileFromExternalLib.class)
.scan("dev.reedworkmc.examples.dependencyinjection");

With to(...), Reedwork creates a new instance whenever the dependency is resolved.

This lets external classes participate in dependency injection without requiring Reedwork-specific annotations.

A dependency can also be created through a factory.

Reedwork.create(this)
.bind(FileFromExternalLib.class)
.toFactory(() -> new FileFromExternalLib(random.nextInt(100)))
.scan("dev.reedworkmc.examples.dependencyinjection");

The factory is invoked whenever Reedwork needs to resolve the dependency.

Factory bindings are useful when creating an object requires custom construction logic, dynamic values, or an object that cannot be created through a simple constructor binding.

This gives you control over object creation while keeping the resulting dependency available through constructor injection.

Sometimes a Paper plugin needs multiple dependencies of the same Java type.

Reedwork supports this through named bindings.

A binding can be assigned a name using .named(String):

Reedwork.create(this)
.bind(FileFromExternalLib.class)
.named("first")
.toSingleton(new FileFromExternalLib(10))
.bind(FileFromExternalLib.class)
.named("second")
.toSingleton(new FileFromExternalLib(20))
.scan("dev.reedworkmc.examples.dependencyinjection");

The corresponding dependency can then be selected with @Named:

public ReedworkService(
@Named("first") FileFromExternalLib first,
@Named("second") FileFromExternalLib second
) {
// ...
}

The name becomes part of the dependency lookup.

This allows multiple bindings of the same Java type to coexist in the same dependency container.

For example, the constructor above receives two different FileFromExternalLib bindings:

first → FileFromExternalLib(10)
second → FileFromExternalLib(20)

Named bindings are useful when the Java type alone is not enough to distinguish between multiple implementations or instances.

When Reedwork creates a component, it examines its constructor and resolves the required parameters from the dependency container.

Consider this command:

public ReedworkCommand(
GreeterService greeterService,
ExecuteCounter executeCounter,
FileFromExternalLib fileFromExternalLib
) {
this.greeterService = greeterService;
this.executeCounter = executeCounter;
this.fileFromExternalLib = fileFromExternalLib;
}

The resulting dependency graph is:

ReedworkCommand
├── GreeterService
├── ExecuteCounter
│ └── FileFromExternalLib
└── FileFromExternalLib

Reedwork resolves the dependencies recursively.

ReedworkCommand requires ExecuteCounter, which itself requires FileFromExternalLib. Reedwork resolves that dependency as part of creating ExecuteCounter.

The exact instance reuse depends on the lifecycle or binding configured for each dependency.

Dependency injection in Paper plugin components

Section titled “Dependency injection in Paper plugin components”

Dependency injection is not limited to services.

Reedwork can inject dependencies into its managed components, including:

  • commands
  • event listeners
  • services
  • suggestion providers
  • other Reedwork-managed components

For example, a Reedwork command can depend directly on an application service:

public final class HelloCommand {
private final GreeterService greeterService;
public HelloCommand(GreeterService greeterService) {
this.greeterService = greeterService;
}
}

The command declares its dependency without needing to construct GreeterService itself.

The same approach can be used when building Paper event listeners and other components managed by Reedwork.

Why use dependency injection in a PaperMC plugin?

Section titled “Why use dependency injection in a PaperMC plugin?”

As a Paper plugin grows, components often need to communicate with shared services.

Without dependency injection, this can lead to manual object creation and increasingly large constructors in plugin initialization code.

For example, a plugin might have to manually create:

GreeterService
ExecuteCounter
PlayerService
DatabaseService
Command instances
Event listener instances

and then pass those objects into the components that require them.

Dependency injection moves that wiring into the dependency container.

Each component instead declares what it needs:

public MyCommand(
PlayerService playerService,
DatabaseService databaseService
) {
this.playerService = playerService;
this.databaseService = databaseService;
}

The component remains responsible for its behavior.

Reedwork is responsible for constructing and connecting the components.

Explicit dependencies improve plugin architecture

Section titled “Explicit dependencies improve plugin architecture”

Constructor injection has another benefit beyond reducing boilerplate: dependencies remain explicit.

A class that requires DatabaseService shows that requirement directly in its constructor.

There is no hidden global lookup such as:

DatabaseService service = Services.get(DatabaseService.class);

Instead, the dependency is part of the component’s Java API.

This makes components easier to understand, test, and reuse.

It also makes the dependency graph of a larger Paper plugin easier to reason about.

The dependency injection system is initialized when Reedwork is created and the plugin package is scanned.

DependencyInjectionExample.java
package dev.reedworkmc.examples.dependencyinjection;
import dev.reedworkmc.examples.externallib.FileFromExternalLib;
import dev.reedworkmc.reedwork.Reedwork;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.Random;
public final class DependencyInjectionExample extends JavaPlugin {
private final Random random = new Random();
@Override
public void onEnable() {
// Plugin startup logic
Reedwork.create(this)
// Singleton:
// Creates one shared FileFromExternalLib instance.
.bind(FileFromExternalLib.class)
.toSingleton(new FileFromExternalLib(42))
// Transient:
// Creates a new FileFromExternalLib instance for each injection.
// .bind(FileFromExternalLib.class)
// .to(FileFromExternalLib.class)
// Factory:
// Creates a new instance using the factory whenever Reedwork resolves it.
// .bind(FileFromExternalLib.class)
// .toFactory(() -> new FileFromExternalLib(random.nextInt(100)))
.scan("dev.reedworkmc.examples.dependencyinjection");
}
}

The scan discovers Reedwork components in the configured package.

Dependencies declared by those components can then be resolved through the configured bindings and component lifecycles.

This keeps the initialization code in the plugin entry point small while the individual components remain responsible for their own behavior.

The lifecycle should reflect how you want the component instance to behave.

  • one shared instance should be used across the plugin
  • the component maintains state
  • creating multiple instances would be unnecessary
  • other components should observe the same component state
  • each resolution should create a new instance
  • the component should not be shared
  • the component is lightweight
  • instance-specific state should not be reused between consumers
  • the class comes from an external library
  • the class cannot be annotated
  • an existing object should be supplied
  • custom construction logic is required
  • multiple implementations or instances need to be distinguished

Reedwork keeps dependency injection close to standard Java.

You declare dependencies through constructors.

You choose component lifecycles with @Singleton and @Transient.

You can explicitly bind external classes when required.

You can use factories for custom object creation.

You can use named bindings when multiple dependencies share the same Java type.

Reedwork handles the dependency graph and object creation.

The result is a Paper plugin architecture where components describe what they need without taking responsibility for how those dependencies are created.

Dependency injection becomes especially useful when combined with Reedwork’s other plugin systems.

Explore:

  • Commands — build PaperMC commands with Java annotations and automatic parameter resolution.
  • Events — automatically discover and register Bukkit and Paper event listeners.
  • Enchantments — create and register custom Minecraft enchantments.
  • Utilities — use reusable utilities for Paper plugin development.

If you are new to Reedwork, start with the introduction and then follow the installation guide.

The complete runnable dependency injection example is available in the ReedworkExamples repository.