A clean, reusable Paper/Java plugin blueprint you can copy whenever you start a new Minecraft plugin.
This project is intentionally built as a strong base, not as a finished gameplay plugin. It gives you the structure most of your plugins will need: commands, permissions, config, messages, listeners, scheduled services, and simple YAML storage.
- Gradle Kotlin DSL project setup
- Paper API dependency
- Modern
plugin.yml - Clean package structure
- Main plugin lifecycle class
- Reloadable
config.yml - Reloadable
messages.yml - MiniMessage support for nice gradients/colors
- Main
/blueprintcommand - Tab completion
- Permissions
- Listener example
- Scheduled task/service example
- Simple YAML storage helper
- Utility class for text components
- README instructions for renaming and extending the project
MinecraftPluginBlueprint/
├── build.gradle.kts
├── settings.gradle.kts
├── README.md
├── LICENSE
├── .gitignore
├── .editorconfig
└── src/
└── main/
├── java/
│ └── me/subzerofrezer/blueprint/
│ ├── BlueprintPlugin.java
│ ├── Permissions.java
│ ├── command/
│ │ └── BlueprintCommand.java
│ ├── config/
│ │ ├── ConfigManager.java
│ │ └── MessageService.java
│ ├── listener/
│ │ └── PlayerJoinListener.java
│ ├── service/
│ │ └── ExampleFeatureService.java
│ ├── storage/
│ │ └── YamlStorage.java
│ └── util/
│ └── Text.java
└── resources/
├── plugin.yml
├── config.yml
└── messages.yml
This blueprint is configured for the current Paper API style shown in the Paper docs:
compileOnly("io.papermc.paper:paper-api:26.1.2.build.+")and Java toolchain:
toolchain.languageVersion.set(JavaLanguageVersion.of(25))If your Minecraft/Paper server is older, change both of these together.
Examples:
compileOnly("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT")
toolchain.languageVersion.set(JavaLanguageVersion.of(21))or for older Java 17 servers:
compileOnly("io.papermc.paper:paper-api:1.20.4-R0.1-SNAPSHOT")
toolchain.languageVersion.set(JavaLanguageVersion.of(17))Always match your plugin API version to the server version you are actually running.
From the project folder:
./gradlew buildOn Windows:
gradlew.bat buildIf you do not have the Gradle wrapper yet, either import the project in IntelliJ and let it use Gradle, or run:
gradle wrapperThen build again.
The compiled plugin JAR will be here:
build/libs/MinecraftPluginBlueprint-1.0.0.jar
Copy that JAR into your Paper server's plugins folder and restart the server.
| Command | Permission | Description |
|---|---|---|
/blueprint help |
blueprint.use |
Shows the help menu |
/blueprint version |
blueprint.use |
Shows the plugin version |
/blueprint reload |
blueprint.reload |
Reloads config/messages/storage and restarts services |
Alias:
/bp
| Permission | Default | Description |
|---|---|---|
blueprint.use |
true |
Allows basic command usage |
blueprint.reload |
op |
Allows /blueprint reload |
blueprint.admin |
op |
Parent admin permission |
src/main/resources/config.yml
settings:
debug: false
example-feature-enabled: true
example-broadcast-interval-seconds: 300
storage:
type: YAMLUse ConfigManager to read values cleanly instead of calling getConfig() everywhere.
Example:
if (plugin.configManager().isExampleFeatureEnabled()) {
// start your feature
}src/main/resources/messages.yml supports MiniMessage formatting.
Example:
prefix: "<gradient:#4FC3F7:#7E57C2><bold>Blueprint</bold></gradient> <dark_gray>»</dark_gray> "
messages:
reload-success: "<green>Configuration and messages reloaded successfully."Send a message like this:
messages.send(sender, "reload-success");With placeholders:
messages.send(sender, "version", Map.of("version", plugin.getPluginMeta().getVersion()));Message path:
messages.version
Placeholder format:
{version}
Suppose you want to create a plugin called FrostGems.
In settings.gradle.kts:
rootProject.name = "FrostGems"In build.gradle.kts:
group = "me.subzerofrezer"
version = "1.0.0"Change the JAR name:
jar {
archiveBaseName.set("FrostGems")
}Current package:
me.subzerofrezer.blueprint
Example new package:
me.subzerofrezer.frostgems
In IntelliJ, right-click the blueprint package and use:
Refactor > Rename
Current:
BlueprintPlugin.javaExample:
FrostGemsPlugin.javaname: FrostGems
main: me.subzerofrezer.frostgems.FrostGemsPlugin
api-version: '26.1.2'In plugin.yml:
commands:
frostgems:
description: Main command for FrostGems.
usage: /frostgems <help|reload|version>
aliases: [fgems]
permission: frostgems.useThen update your Java command registration:
PluginCommand command = getCommand("frostgems");For small plugins, you can add another case inside BlueprintCommand.
For bigger plugins, create a separate command class:
command/
├── BlueprintCommand.java
└── GemsCommand.java
Then register it inside BlueprintPlugin#registerCommands().
Example:
PluginCommand gemsCommand = getCommand("gems");
if (gemsCommand != null) {
GemsCommand executor = new GemsCommand(this, messageService);
gemsCommand.setExecutor(executor);
gemsCommand.setTabCompleter(executor);
}And add the command to plugin.yml.
Create a listener class:
public final class BlockBreakListener implements Listener {
@EventHandler
public void onBlockBreak(BlockBreakEvent event) {
event.getPlayer().sendMessage("You broke a block!");
}
}Register it in BlueprintPlugin#registerListeners():
getServer().getPluginManager().registerEvents(new BlockBreakListener(), this);Services are good for systems that have their own logic, such as:
- currency systems
- minion systems
- pet systems
- crate systems
- cooldown systems
- shop systems
- database sync systems
- scheduled tasks
Example:
public final class GemsService {
private final BlueprintPlugin plugin;
public GemsService(BlueprintPlugin plugin) {
this.plugin = plugin;
}
public void giveGems(UUID playerId, int amount) {
// business logic here
}
}Create the service in your main plugin class and expose it with a getter.
The blueprint creates a data.yml file automatically.
Example write:
plugin.storage().data().set("players." + uuid + ".gems", 100);
plugin.storage().save();Example read:
int gems = plugin.storage().data().getInt("players." + uuid + ".gems", 0);This is fine for small plugins or prototypes.
For larger plugins, replace this with MySQL, MariaDB, PostgreSQL, SQLite, Redis, or MongoDB depending on the project.
For small plugins:
command/
listener/
config/
storage/
util/
For serious plugins:
api/
command/
config/
database/
dto/
event/
gui/
listener/
model/
repository/
service/
task/
util/
Example for a premium currency plugin:
frostpoints/
├── api/
│ └── FrostPointsApi.java
├── command/
│ └── PointsCommand.java
├── database/
│ └── MySqlConnectionProvider.java
├── gui/
│ └── PointsShopGui.java
├── listener/
│ └── PlayerJoinListener.java
├── model/
│ └── PlayerBalance.java
├── repository/
│ └── BalanceRepository.java
├── service/
│ └── PointsService.java
└── task/
└── AutoSaveTask.java
- Keep your main plugin class small.
- Put business logic in services.
- Put config reading in config classes.
- Put database/storage logic in repositories or storage classes.
- Never do heavy database or web API work on the main server thread.
- Always validate command input.
- Always check permissions.
- Always cancel repeating tasks on disable.
- Keep messages in
messages.yml, not hardcoded everywhere. - Test one feature at a time before adding more.
Most Bukkit/Paper API calls must run on the main server thread.
Safe async examples:
- database queries
- web API requests
- file parsing
- calculations
Unsafe async examples:
- changing blocks
- opening inventories
- teleporting players
- modifying entities
- sending many direct Bukkit API calls
Pattern:
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
// slow database/web work here
Bukkit.getScheduler().runTask(plugin, () -> {
// Bukkit API work here
});
});Your plugin was compiled with a newer Java version than your server runs.
Fix:
- Check your server Java version with
java -version. - Lower
options.release.set(...)andtoolchain.languageVersion.set(...)if your Paper version supports it. - Or update the Java runtime used by your server.
The plugin failed during startup.
Fix:
- Check the first error in console, not only the command error.
- Make sure
plugin.ymlhas the correctmainpath. - Make sure your package/class names match exactly.
- Make sure your dependency version matches your server version.
Check:
- Is the command inside
plugin.yml? - Is it registered in
registerCommands()? - Does the player have permission?
- Did the plugin load green in
/plugins?
Your command name in Java does not match the command name in plugin.yml.
Once the base is working, good upgrades are:
- Add a proper subcommand framework
- Add GUI base classes
- Add cooldown manager
- Add MySQL/SQLite repository layer
- Add Vault economy hook
- Add PlaceholderAPI expansion
- Add LuckPerms integration
- Add unit tests for pure Java service logic
- Add GitHub Actions build workflow
- Add release packaging
Build:
./gradlew buildClean build:
./gradlew clean buildFind output JAR:
build/libs/
MIT. You can copy, rename, modify, and use this as the base for your own private or public Minecraft plugins.