JExHibernate — Hibernate for plugins, Spring Boot, and everything in between
Originally meant as a wrapper for Minecraft plugins. Today JExHibernate also clears about 65% of the database boilerplate out of Spring Boot projects and standalone Java apps.
I started writing JExHibernate two years ago because the state of persistence code in Minecraft plugins annoyed me too much. What began as a small wrapper is today a real library I also run in production Spring Boot services. Repository: github.com/JExcellence/JEHibernate (the artifact is named JEHibernate for historical reasons; in conversation I call it JExHibernate because it belongs to the JExcellence family).
This post explains what the library does, why it exists, and how it behaves in the three worlds it regularly runs in: Bukkit/Paper/Folia, Spring Boot, and standalone Java.
What JExHibernate is
A thin, opinionated wrapper around Hibernate ORM 7.x with a fluent API that, by my own measurement, removes about 65% of the boilerplate you write in a typical JPA project — repository classes, pagination, query builder, caching, transaction handling, async variants of everything. Java 17+, with automatic use of virtual threads as soon as the runtime is Java 21 or newer.
Eight databases are supported out of the box: H2, MySQL, MariaDB, PostgreSQL, Oracle, SQL Server, SQLite, and HSQLDB. Connection pooling via Agroal, second-level cache via JCache — both optional, both with sensible defaults.
Three worlds, one wrapper
1. Spigot, Paper, Folia
This is where JExHibernate was born. The plugin environment has three specific quirks that take down any unprotected Hibernate integration:
- Class loaders. Bukkit, Paper, and Folia load every plugin with its own ClassLoader. Hibernate expects all entity classes under a canonical loader. JExHibernate handles the thread-context switching internally so plugin developers don't have to debug it themselves.
- Main-thread hygiene. Every repository method has an async variant returning a
CompletableFuture. That's not just convenience — on a server that has to tick 20 times a second, it's the line between "runs" and "stutters under load". - Folia awareness. Folia (PaperMC's regionalised threading branch) changes the main-thread assumptions. The repository layer is region-agnostic; the async calls work identically across all three servers.
Setup in a plugin's onEnable:
public class MyPlugin extends JavaPlugin {
private JEHibernate jeHibernate;
@Override
public void onEnable() {
saveResource("database/hibernate.properties", false);
jeHibernate = JEHibernate.builder()
.configuration(config -> config.fromProperties(
PropertyLoader.load(
getDataFolder(), "database", "hibernate.properties"
)))
.scanPackages("com.example.myplugin")
.build();
var playerRepo = jeHibernate.repositories().get(PlayerRepository.class);
playerRepo.preloadAsync();
}
@Override
public void onDisable() {
jeHibernate.close();
}
}Server admins switch databases by editing the supplied hibernate.properties — no recompilation: database.type=H2 for an embedded development DB, database.type=MYSQL for the production box. Six more vendors work the same way.
2. Spring Boot
Spring Data JPA is excellent — as long as you stay within the Spring universe. The moment you want to share code between plugins and Spring services (which I do often, because a web dashboard and the corresponding plugin reach for the same entities), the friction starts.
JExHibernate works in Spring Boot as a regular @Bean and integrates cleanly into the Spring lifecycle via @PreDestroy:
@Configuration
public class JEHibernateConfig {
@Bean
public JEHibernate jeHibernate() {
return JEHibernate.builder()
.configuration(config -> config
.database(DatabaseType.POSTGRESQL)
.url("jdbc:postgresql://localhost:5432/mydb")
.credentials("user", "pass")
.ddlAuto("validate")
.connectionPool(5, 20))
.scanPackages("com.example")
.build();
}
@Bean
public PlayerRepository playerRepository(JEHibernate jeh) {
return jeh.repositories().get(PlayerRepository.class);
}
@PreDestroy
public void shutdown(JEHibernate jeh) { jeh.close(); }
}What Spring Boot saves: no spring-boot-starter-data-jpa dependency, no @EnableJpaRepositories ceremony, no property magic across three different application.yml profiles. Instead, a single bean configured identically everywhere — plugin or service.
3. Standalone Java / CLI tools
For CLI tools, migration scripts, and ad-hoc data fixes, it works the same. JEHibernate.fromProperties(...) in the main method, done. No container, no servlet stack, no annotation magic.
The features that actually matter in practice
Repositories without boilerplate
A complete repository class is three lines:
public class PlayerRepository extends AbstractCrudRepository<PlayerData, UUID> {
public PlayerRepository(ExecutorService ex, EntityManagerFactory emf, Class<PlayerData> cls) {
super(ex, emf, cls);
}
}With that you have findById, findAll, save, create, update, delete, refresh, exists, count, plus every async variant, plus batch operations (saveAll, deleteAll), plus pagination with PageResult metadata, Specifications, and a type-safe Query Builder.
Query Builder — no SQL, no JPQL
var richActive = repo.query()
.and("active", true)
.greaterThan("balance", 10_000)
.like("username", "%alice%")
.in("rank", List.of("VIP", "ADMIN"))
.orderByDesc("balance")
.fetch("inventory") // INNER JOIN FETCH
.fetchLeft("guild") // LEFT JOIN FETCH (nullable)
.getPage(0, 20);
richActive.totalElements(); // 14_823
richActive.totalPages(); // 742
richActive.hasNext(); // trueThis is where JExHibernate clearly differentiates itself from a raw Hibernate setup: no more accidental N+1, no LazyInitializationException drama, no manual page counting in a second query.
Cached repositories
For lookups that happen often but rarely change (profiles, leaderboards, permissions), there's AbstractCachedRepository — a dual layer of Caffeine caches with ID and custom-key lookup, stale-while-revalidate, TTL jitter against stampede problems, and automatic invalidation on mutations.
public class PlayerRepository
extends AbstractCachedRepository<PlayerData, UUID, String> {
public PlayerRepository(ExecutorService ex, EntityManagerFactory emf, Class<PlayerData> cls) {
super(ex, emf, cls,
PlayerData::getUsername, // cache key
CacheConfig.builder()
.expiration(Duration.ofMinutes(30))
.refreshAfterWrite(Duration.ofMinutes(25))
.maxSize(5000)
.jitterPercent(10)
.build());
}
}On a server with ten thousand players, that's the difference between a 5 ms lookup and a 200 ns one.
Optimistic lock retry
Concurrent updates to the same entity become reality the moment more than one thread touches the data. Instead of wrapping every call in try/catch:
OptimisticLockRetry.execute(() -> {
var p = repo.findByIdOrThrow(uuid);
p.setBalance(p.getBalance() + amount);
return repo.save(p);
});Three retries with exponential backoff by default. Optionally also enabled for database deadlocks.
Slow query detection
Every query that runs longer than 500 ms is automatically logged at WARN level. The threshold is configurable. One of the few features I use every day in production — on a live server it's often the first thing that warns of an approaching performance wall.
What JExHibernate deliberately does not do
- No DI framework of its own. There is a small
@Injectimplementation for ad-hoc wiring, but if you use Spring or Guice you keep them. - No REST layer. If you need that, pair JExHibernate with Javalin, Spark, or Spring — which I regularly do for web dashboards.
- No multi-server synchronisation. Event bus or Redis pub/sub are out of scope.
Where it's heading
Current version (May 2026): 3.0.1 with Hibernate 7.x and Jakarta Persistence 3.1+. 78 tests, all green.
On the roadmap:
- Better test tooling — a
@RepositoryTestannotation that spins up an in-memory H2 plus schema migrations automatically. - Deeper Folia integration. The repository layer is region-agnostic today; lifecycle management will be surfaced more cleanly in a Folia-specific way.
- More database dialects if demand turns up — CockroachDB and YugabyteDB are next on the list.
If you build plugins, Spring services, or standalone tools with real persistence and don't want to write the same boilerplate every time, give JExHibernate a look. Pull requests welcome — the code is intentionally compact and fully documented.
Repository: github.com/JExcellence/JEHibernate · Apache License 2.0 · 78 tests · Java 17+, Hibernate 7.x.