Reactive JPA persistence for GuicedEE applications using Hibernate Reactive 7 and Vert.x 5.
Extend DatabaseModule, point it at a persistence.xml unit, and the module wires a Mutiny.SessionFactory into Guice β fully reactive, annotation-driven, with built-in support for PostgreSQL, MySQL, SQL Server, Oracle, and DB2.
Built on Hibernate Reactive Β· Vert.x SQL Client Β· Google Guice Β· Mutiny Β· JPMS module com.guicedee.persistence Β· Java 25+
<dependency>
<groupId>com.guicedee</groupId>
<artifactId>persistence</artifactId>
</dependency>Gradle (Kotlin DSL)
implementation("com.guicedee:persistence:2.0.0-SNAPSHOT")- Annotation-driven persistence units β extend
DatabaseModule, annotate with@EntityManager, and Guice wires everything frompersistence.xml - Hibernate Reactive + Mutiny β
Mutiny.SessionFactoryis bound in Guice with full reactive session/transaction support - Multi-database support β built-in
ConnectionBaseInfoimplementations for PostgreSQL, MySQL, SQL Server, Oracle, and DB2 - Environment variable resolution β
${VAR_NAME}placeholders inpersistence.xmlproperties are resolved from system properties or environment variables - Vert.x SQL Client pooling β pre-initialized shared connection pools on the Vert.x event loop for optimal Hibernate Reactive integration
- Multiple persistence units β bind multiple
DatabaseModulesubclasses with distinct@Namedqualifiers; one is marked as the default @EntityManagerscoping β annotate packages or classes to associate entities with specific persistence units- SPI-driven property processing β
IPropertiesEntityManagerReaderandIPropertiesConnectionInfoReadercontribute database-specific Hibernate settings - Vert.x context-aware startup β
EntityManagerFactorycreation runs on a proper Vert.x context to satisfy Hibernate Reactive's internal requirements - Lifecycle management β
PersistService.start()/stop()integrated withIGuicePostStartup/IGuicePreDestroy
Step 1 β Add a persistence.xml:
<!-- src/main/resources/META-INF/persistence.xml -->
<persistence xmlns="https://jakarta.ee/xml/ns/persistence" version="3.0">
<persistence-unit name="mydb">
<provider>org.hibernate.reactive.provider.ReactivePersistenceProvider</provider>
<class>com.example.entities.User</class>
<properties>
<property name="jakarta.persistence.jdbc.url"
value="${DB_URL:jdbc:postgresql://localhost:5432/mydb}"/>
<property name="jakarta.persistence.jdbc.user" value="${DB_USER:postgres}"/>
<property name="jakarta.persistence.jdbc.password" value="${DB_PASSWORD:secret}"/>
<property name="hibernate.hbm2ddl.auto" value="update"/>
</properties>
</persistence-unit>
</persistence>Step 2 β Create a DatabaseModule subclass:
public class MyDatabaseModule extends DatabaseModule<MyDatabaseModule> {
@Override
protected String getPersistenceUnitName() {
return "mydb";
}
@Override
protected ConnectionBaseInfo getConnectionBaseInfo(
PersistenceUnitDescriptor unit, Properties properties) {
return ConnectionBaseInfoFactory.createConnectionBaseInfo("postgresql");
}
}Step 3 β Register via JPMS:
module my.app {
requires com.guicedee.persistence;
provides com.guicedee.client.services.lifecycle.IGuiceModule
with my.app.MyDatabaseModule;
}Step 4 β Use the reactive session factory:
public class UserService {
@Inject
private Mutiny.SessionFactory sessionFactory;
public Uni<User> createUser(String name) {
User user = new User();
user.setName(name);
return sessionFactory.withTransaction(session ->
session.persist(user).replaceWith(user)
);
}
public Uni<User> findUser(Long id) {
return sessionFactory.withSession(session ->
session.find(User.class, id)
);
}
}Startup
IGuiceContext.instance()
ββ IGuiceConfigurator hooks
ββ GuicedConfigurator (enables annotation, field, method scanning)
ββ IGuiceModule hooks
ββ MyDatabaseModule (extends DatabaseModule)
ββ Parse persistence.xml (PersistenceXmlParser)
ββ IPropertiesEntityManagerReader SPIs
β ββ SystemEnvironmentVariablesPropertiesReader (${VAR} resolution)
β ββ HibernateEntityManagerProperties
β ββ PostgresHibernateProperties / MySql... / Oracle... / SqlServer... / DB2...
ββ IPropertiesConnectionInfoReader SPIs
β ββ HibernateDefaultConnectionBaseBuilder (maps jakarta.persistence.jdbc.*)
ββ ConnectionBaseInfo.populateFromProperties()
ββ ConnectionBaseInfo.toPooledDatasource() (Vert.x SQL pool init)
ββ JtaPersistModule
ββ bind PersistService @Named("mydb")
ββ bind Mutiny.SessionFactory @Named("mydb") + default
ββ VertxPersistenceModule
ββ Validate @EntityManager annotations
ββ Process package-level @EntityManager
ββ Bind default EntityManager / SessionFactory
ββ IGuicePostStartup hooks
ββ DatabaseModule.postLoad()
ββ PersistService.start() (creates EntityManagerFactory on Vert.x context)
ββ IGuicePreDestroy hooks
ββ DatabaseModule.onDestroy()
ββ PersistService.stop() (closes SessionFactory + EntityManagerFactory)
Hibernate Reactive
ββ ServiceContributor SPI
ββ VertxServiceContributor
ββ Registers VertxInstance backed by VertXPreStartup.getVertx()
β Hibernate Reactive uses the shared Vert.x instance for all reactive IO
Every persistence unit is represented by a DatabaseModule subclass annotated with @EntityManager:
public class OrdersDatabaseModule extends DatabaseModule<OrdersDatabaseModule> {
@Override
protected String getPersistenceUnitName() {
return "orders";
}
@Override
protected ConnectionBaseInfo getConnectionBaseInfo(
PersistenceUnitDescriptor unit, Properties properties) {
return ConnectionBaseInfoFactory.createConnectionBaseInfo("postgresql");
}
@Override
public Integer sortOrder() {
return 50; // controls startup ordering
}
}| Attribute | Default | Purpose |
|---|---|---|
value |
"" |
Persistence unit name (maps to persistence.xml) |
allClasses |
true |
Include all entity classes or only the annotated package |
defaultEm |
true |
Mark as the default SessionFactory binding |
Apply at class level (on DatabaseModule subclasses) or package level (package-info.java) to scope entities to specific persistence units.
Abstract base class carrying all JDBC connection properties. Database-specific subclasses provide the toPooledDatasource() method that creates a Vert.x SqlClient pool.
Creates the correct ConnectionBaseInfo for a given database type:
// By database name
ConnectionBaseInfo cbi = ConnectionBaseInfoFactory.createConnectionBaseInfo("postgresql");
// By JDBC URL (auto-detects database type)
ConnectionBaseInfo cbi = ConnectionBaseInfoFactory.createConnectionBaseInfoFromJdbcUrl(
"jdbc:postgresql://localhost:5432/mydb");| Database | Type string | ConnectionBaseInfo class |
Hibernate properties |
|---|---|---|---|
| PostgreSQL | postgresql, postgres |
PostgresConnectionBaseInfo |
PostgresHibernateProperties |
| MySQL / MariaDB | mysql, mariadb |
MySqlConnectionBaseInfo |
MySqlHibernateProperties |
| SQL Server | sqlserver, mssql |
SqlServerConnectionBaseInfo |
SqlServerHibernateProperties |
| Oracle | oracle |
OracleConnectionBaseInfo |
OracleHibernateProperties |
| DB2 | db2 |
DB2ConnectionBaseInfo |
DB2HibernateProperties |
| Property | Default | Purpose |
|---|---|---|
serverName |
β | Database server hostname |
port |
varies | Database server port |
databaseName |
β | Database / schema name |
username |
β | Authentication username |
password |
β | Authentication password |
minPoolSize |
1 |
Minimum connection pool size |
maxPoolSize |
5 |
Maximum connection pool size |
maxIdleTime |
β | Idle connection timeout (seconds) |
maxLifeTime |
β | Maximum connection lifetime (seconds) |
reactive |
true |
Use Hibernate Reactive (vs. blocking) |
defaultConnection |
true |
Register as the default binding |
Standard JPA/Jakarta persistence properties are supported:
| Property | Purpose |
|---|---|
jakarta.persistence.jdbc.url |
JDBC connection URL |
jakarta.persistence.jdbc.user |
Database username |
jakarta.persistence.jdbc.password |
Database password |
jakarta.persistence.jdbc.driver |
JDBC driver class |
hibernate.hbm2ddl.auto |
Schema management (update, validate, create, create-drop) |
hibernate.dialect |
Hibernate dialect (auto-set by database-specific readers) |
All persistence.xml property values support ${VAR_NAME} and ${VAR_NAME:default} syntax:
<property name="jakarta.persistence.jdbc.url"
value="${DB_URL:jdbc:postgresql://localhost:5432/mydb}"/>
<property name="jakarta.persistence.jdbc.user"
value="${DB_USER:postgres}"/>
<property name="jakarta.persistence.jdbc.password"
value="${DB_PASSWORD}"/>The SystemEnvironmentVariablesPropertiesReader resolves placeholders in this order:
- System property (
-DDB_URL=...) - Environment variable (
export DB_URL=...) - Default value (after the
:separator)
Kubernetes-friendly: dot-notation properties (e.g., db.url) are also tried as uppercase underscored (DB_URL).
All SPIs are discovered via ServiceLoader. Register implementations with JPMS provides...with or META-INF/services.
Contributes database-specific Hibernate properties for a persistence unit:
public class MyCustomProperties
implements IPropertiesEntityManagerReader<MyCustomProperties> {
@Override
public boolean applicable(PersistenceUnitDescriptor pu) {
return "mydb".equals(pu.getName());
}
@Override
public Map<String, String> processProperties(
PersistenceUnitDescriptor pu, Properties properties) {
return Map.of("hibernate.jdbc.batch_size", "100");
}
}Populates ConnectionBaseInfo from persistence unit properties:
public class MyConnectionReader
implements IPropertiesConnectionInfoReader<MyConnectionReader> {
@Override
public ConnectionBaseInfo populateConnectionBaseInfo(
PersistenceUnitDescriptor unit, Properties props,
ConnectionBaseInfo cbi) {
cbi.setMaxPoolSize(20);
return cbi;
}
}| SPI | Purpose |
|---|---|
IPropertiesEntityManagerReader |
Contribute database-specific Hibernate properties |
IPropertiesConnectionInfoReader |
Populate ConnectionBaseInfo from persistence properties |
IGuiceConfigurator |
Configure classpath scanning (enabled by GuicedConfigurator) |
ServiceContributor (Hibernate) |
Bridge the Vert.x instance into Hibernate Reactive |
| Type | Qualifier | Scope | Purpose |
|---|---|---|---|
Mutiny.SessionFactory |
@Named("puName") |
Singleton | Named session factory for a specific persistence unit |
Mutiny.SessionFactory |
(none) | Singleton | Default session factory (from defaultEm = true) |
PersistService |
@Named("puName") |
Singleton | Lifecycle service (start() / stop()) |
PersistService |
(none) | Singleton | Default persistence service |
public class MultiDbService {
@Inject
private Mutiny.SessionFactory defaultFactory; // from defaultEm = true
@Inject
@Named("orders")
private Mutiny.SessionFactory ordersFactory; // specific PU
@Inject
@Named("analytics")
private Mutiny.SessionFactory analyticsFactory; // another PU
}// Read with a session
sessionFactory.withSession(session ->
session.find(User.class, userId)
).subscribe().with(
user -> log.info("Found: {}", user),
err -> log.error("Failed", err)
);
// Write inside a transaction
sessionFactory.withTransaction(session ->
session.persist(newUser)
.chain(() -> session.persist(newOrder))
).subscribe().with(
v -> log.info("Committed"),
err -> log.error("Rolled back", err)
);<persistence>
<persistence-unit name="users">
<class>com.example.entities.User</class>
<properties>
<property name="jakarta.persistence.jdbc.url" value="${USERS_DB_URL}"/>
...
</properties>
</persistence-unit>
<persistence-unit name="orders">
<class>com.example.entities.Order</class>
<properties>
<property name="jakarta.persistence.jdbc.url" value="${ORDERS_DB_URL}"/>
...
</properties>
</persistence-unit>
</persistence>@EntityManager(value = "users", defaultEm = true)
public class UsersDatabaseModule extends DatabaseModule<UsersDatabaseModule> {
@Override protected String getPersistenceUnitName() { return "users"; }
@Override protected ConnectionBaseInfo getConnectionBaseInfo(
PersistenceUnitDescriptor unit, Properties props) {
return ConnectionBaseInfoFactory.createConnectionBaseInfo("postgresql");
}
}
@EntityManager(value = "orders", defaultEm = false)
public class OrdersDatabaseModule extends DatabaseModule<OrdersDatabaseModule> {
@Override protected String getPersistenceUnitName() { return "orders"; }
@Override protected ConnectionBaseInfo getConnectionBaseInfo(
PersistenceUnitDescriptor unit, Properties props) {
return ConnectionBaseInfoFactory.createConnectionBaseInfo("mysql");
}
}@EntityManager(value = "orders")
package com.example.entities.orders;
import com.guicedee.persistence.annotations.EntityManager;IGuiceContext.instance()
ββ IGuiceConfigurator hooks
ββ GuicedConfigurator (enables rich classpath scanning)
ββ IGuiceModule hooks
ββ DatabaseModule subclasses (parse persistence.xml, build ConnectionBaseInfo)
ββ JtaPersistModule (bind PersistService + Mutiny.SessionFactory)
ββ VertxPersistenceModule (validate, bind defaults, process @EntityManager)
ββ Hibernate ServiceContributor SPI
ββ VertxServiceContributor (registers shared Vertx instance)
ββ IGuicePostStartup hooks
ββ DatabaseModule.postLoad() (PersistService.start() β EntityManagerFactory)
ββ IGuicePreDestroy hooks
ββ DatabaseModule.onDestroy() (PersistService.stop() β close factory)
com.guicedee.persistence
βββ org.hibernate.reactive (Hibernate Reactive β Mutiny.SessionFactory)
βββ org.hibernate.orm.core (Hibernate ORM β persistence.xml parsing, entity management)
βββ com.guicedee.vertx (Vert.x lifecycle, VertXPreStartup)
βββ com.guicedee.guicedinjection (GuicedEE β scanning, DI, lifecycle)
βββ com.guicedee.microprofile.config (MicroProfile Config β @ConfigProperty injection, SmallRye Config)
βββ io.vertx.sql.client (Vert.x SQL Client β pooled connections)
βββ io.vertx.sql.client.pg (Vert.x PostgreSQL client β optional)
βββ io.vertx.sql.client.mssql (Vert.x MSSQL client β optional)
βββ jakarta.transaction (JTA transactions)
βββ com.guicedee.rest (REST integration β optional)
Module name: com.guicedee.persistence
The module:
- exports
com.guicedee.persistence,com.guicedee.persistence.annotations,com.guicedee.persistence.bind,com.guicedee.persistence.implementations, and database-specific packages - provides
IGuiceConfiguratorwithGuicedConfigurator - provides
IPropertiesEntityManagerReaderwithSystemEnvironmentVariablesPropertiesReader,HibernateEntityManagerProperties,PostgresHibernateProperties,MySqlHibernateProperties,OracleHibernateProperties,SqlServerHibernateProperties,DB2HibernateProperties - provides
IPropertiesConnectionInfoReaderwithHibernateDefaultConnectionBaseBuilder - provides
ServiceContributorwithVertxServiceContributor - uses
IPropertiesConnectionInfoReader,IPropertiesEntityManagerReader
In non-JPMS environments, META-INF/services discovery still works.
| Class | Package | Role |
|---|---|---|
DatabaseModule |
vertxpersistence |
Abstract Guice module β extend per persistence unit; lifecycle, config, and binding |
ConnectionBaseInfo |
vertxpersistence |
Abstract connection configuration β host, port, credentials, pool settings |
ConnectionBaseInfoFactory |
vertxpersistence |
Factory β creates database-specific ConnectionBaseInfo by type or JDBC URL |
ConnectionBaseInfoBuilder |
vertxpersistence |
Maps jakarta.persistence.jdbc.* properties into ConnectionBaseInfo |
PersistService |
vertxpersistence |
Interface β start() / stop() lifecycle for EntityManagerFactory |
@EntityManager |
annotations |
Binding annotation β names persistence units, controls default binding |
JtaPersistModule |
bind |
Internal Guice module β binds PersistService, Mutiny.SessionFactory, properties |
JtaPersistService |
bind |
Manages EntityManagerFactory creation and shutdown via Mutiny Uni |
VertxPersistenceModule |
implementations |
Guice module β validates @EntityManager annotations, binds defaults |
VertxServiceContributor |
implementations |
Hibernate ServiceContributor β bridges shared Vert.x instance |
GuicedConfigurator |
implementations |
IGuiceConfigurator β enables classpath scanning for persistence |
SystemEnvironmentVariablesPropertiesReader |
implementations |
Resolves ${VAR:default} placeholders in persistence properties |
PostgresConnectionBaseInfo |
implementations.postgres |
PostgreSQL-specific Vert.x SQL pool creation |
MySqlConnectionBaseInfo |
implementations.mysql |
MySQL-specific Vert.x SQL pool creation |
SqlServerConnectionBaseInfo |
implementations.sqlserver |
SQL Server-specific Vert.x SQL pool creation |
OracleConnectionBaseInfo |
implementations.oracle |
Oracle-specific Vert.x SQL pool creation |
DB2ConnectionBaseInfo |
implementations.db2 |
DB2-specific Vert.x SQL pool creation |
Use Testcontainers for integration tests with real databases:
@Testcontainers
public class UserRepositoryTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");
@BeforeAll
static void setup() {
System.setProperty("DB_URL", postgres.getJdbcUrl());
System.setProperty("DB_USER", postgres.getUsername());
System.setProperty("DB_PASSWORD", postgres.getPassword());
IGuiceContext.registerModuleForScanning.add("com.example");
IGuiceContext.instance();
}
}Issues and pull requests are welcome β please add tests for new database adapters, connection options, or SPI implementations.