For Server Developers

Paper Plugin Development Best Practices

Paper plugin best practices for 26.3: what still holds, and what the 26.x remapper removal, Java 25 and new versioning scheme broke.

Most Paper plugin best practices you'll find were written for the 1.16–1.20 era, and they are still broadly correct about architecture. They are no longer correct about mappings, Java versions, gamerule identifiers, or version parsing. This page separates the two: what the 26.x changes broke, then the practices that have held for years. It was last re-checked against 26.3, released September 15, 2026.

Breaking change: Paper dropped its internal remapper in 26.1

As of 26.1, Paper has fully dropped its internal remapper. Obfuscated names are gone, and there is no runtime fallback — a plugin compiled against Spigot mappings will not load. If you have NMS or internals usage anywhere in your codebase, this is a hard migration, not a warning.

The migration path is the userdev Gradle plugin, which compiles against Mojang-mapped Paper. Two pieces of practical advice while you're in there: audit every NMS touchpoint and record why it exists, and replace each one with API where an API path exists. Internals usage that survives a mapping migration will simply cost you again at the next one.

Nothing about this changed in 26.3. Paper's userdev documentation still marks the reobfJar reobfuscation path as unsupported from 26.1 onward, and the dev bundle dependency now takes the form paperweight.paperDevBundle("26.3.build.+").

Java 25 is required from 26.1 onward

Versions 1.20 through 1.21.11 run on Java 21. From 26.1 onward, Java 25 is required, and 26.3 keeps that minimum. Set the toolchain explicitly in your build rather than relying on whatever JDK your machine defaults to, and pin the same version in CI so a passing build actually means something. If you're setting up the server side of this too, how to make a Minecraft server has the full Java version table.

Gamerules became a registry in 1.21.11

1.21.11 turned gamerules into a registry and renamed them from camelCase to snake_case, with some merged or renamed. The lesson generalises: gamerule identifiers are version-sensitive data, not constants. Keep them in configuration or a version-keyed map, not scattered as string literals through your codebase.

Version parsing: "1." is no longer a safe prefix

Version strings are now year.drop.hotfix — 26.2, 26.1.2 — with no leading "1.". Any code that string-matches on "1." or splits on the assumption of a leading major of 1 is broken. Parse defensively, and prefer feature detection (does this class/method/registry entry exist?) over version comparison wherever you can.

Paper's own artifacts changed shape too. Since 26.1 the old -R0.1-SNAPSHOT suffix is gone; API versions now look like 26.1.2.build.23-alpha, i.e. <mcversion>.build.<build>-<status>. To track the latest build of one version, depend on 26.3.build.+. PaperMC warns that dropping the build part — 26.1.+, say — can resolve to a different and possibly breaking version such as 26.1.1.

26.3 migration notes

26.3 is a quiet release for plugin authors: no mapping change, no Java change, no new remapper rules. What did change:

For the player-facing side of the release, see our snapshot roundup and 26.3 recap.

Handle the paused-server edge case

pause-when-empty-seconds arrived in vanilla 1.21.2 with a default of 60 seconds. Paper disables it by default (-1) because it is incompatible with what plugins expect, but an admin can turn it on. When it's on and the server is empty, the world stops ticking, and any logic built on tick-based timers silently stalls. If your plugin schedules work that must happen on a wall-clock cadence, don't assume ticks advance — and document the assumption either way. Paper's 1.21.3 update added API for exactly this: check Server#isPaused, and if your plugin cannot support pausing at all, use Server#allowPausing(Plugin, Boolean) to stop admins enabling it by accident. See server.properties, fully explained for the admin-side behaviour.

Main-thread rules

The Bukkit/Paper API is not thread-safe. The pattern that works: gather the data you need on the main thread, do the slow work asynchronously, then schedule a sync task to apply results. Never touch the API from a JDBC callback or an HTTP client thread — it will work in testing and corrupt state under load. If you target Folia, note that its threading model is regional and these assumptions change again; check Folia's own documentation before porting.

Store plugin data in PersistentDataContainer

PDC is the correct home for per-entity, per-block-entity and per-item plugin data. It survives restarts, it doesn't fight other plugins the way lore-tag hacks do, and it's stable API. Keep NamespacedKey hygiene tight: one namespace for your plugin, descriptive key names, and never reuse a key for a different data shape across versions.

Use the modern Brigadier-based command API

Paper's Brigadier-based command API gives you typed arguments, real suggestions and server-side validation instead of hand-parsing String[] args. The caveat is portability: it's Paper-only, so it will not work on Spigot, and Fabric's command system is a different thing entirely. If cross-platform support matters to you, isolate command handling behind your own interface.

Event listener hygiene

Testing without a live server

The parts of a plugin most likely to break across versions — version string parsing, gamerule name mapping, config migration — are also the parts easiest to test in isolation. Decouple that logic from Bukkit types and unit test it. You get regression coverage for exactly the class of breakage 26.1 and 1.21.11 just handed everyone.

Summary

AreaWhat to do
MappingsMigrate to userdev / Mojang mappings; remapper is gone as of 26.1 (unchanged in 26.3).
Java21 for 1.20–1.21.11; 25 from 26.1 onward, still 25 on 26.3. Pin the toolchain.
GamerulesTreat identifiers as versioned data; snake_case since 1.21.11.
VersionsParse year.drop.hotfix defensively; prefer feature detection; depend on 26.3.build.+, not a bare 26.3.+.
PausingOff by default on Paper; check Server#isPaused, opt out with Server#allowPausing.
ThreadingGather sync, work async, apply sync. Folia changes the rules.
DataPersistentDataContainer with disciplined NamespacedKeys.
CommandsBrigadier API on Paper; abstract it if you need portability.