Most Paper plugin advice you'll find was written for the 1.16–1.20 era and is still broadly correct about architecture. It is 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.
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.
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. 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.
Handle the paused-server edge case
pause-when-empty-seconds has existed since 1.21.3, and while Paper defaults it off, an admin can turn it on. When it's on and the server is empty, the world stops ticking. 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. 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
- Listen to the narrowest event that gives you what you need.
- Keep handlers short — event handlers run on the main thread.
- Use
ignoreCancelled = truerather than re-checking cancellation. - Respect
EventPriority; useMONITORfor observation only, and never modify state there. - Unregister listeners on disable, especially for dynamically registered ones.
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
| Area | What to do |
|---|---|
| Mappings | Migrate to userdev / Mojang mappings; remapper is gone as of 26.1. |
| Java | 21 for 1.20–1.21.11; 25 from 26.1 onward. Pin the toolchain. |
| Gamerules | Treat identifiers as versioned data; snake_case since 1.21.11. |
| Versions | Parse year.drop.hotfix defensively; prefer feature detection. |
| Threading | Gather sync, work async, apply sync. Folia changes the rules. |
| Data | PersistentDataContainer with disciplined NamespacedKeys. |
| Commands | Brigadier API on Paper; abstract it if you need portability. |