Skip to content

Persistence

View Markdown

Vertique persistence is PostgreSQL on Vert.x’s non-blocking SQL client, wrapped in repositories you own:

  • A working application from one command. The REST/PostgreSQL archetype generates a CRUD application with a repository, a Flyway migration, and a real-database integration test.
  • Reactive repositories with fluent queries. A repository borrows from an injected non-blocking connection pool and builds statements with a typed query builder that covers parameters, row mapping, and paging.
  • Typed failure translation. Database failures are translated into a typed exception hierarchy before they propagate. A unique-constraint violation arrives as a UniqueConstraintViolationException instead of a raw SQL state.
  • Transactions as a seam. A fluent transaction builder runs multi-statement work atomically. It commits automatically on success, rolls back automatically on failure, and applies the same exception translation single statements get.
  • Migrations at startup, owned by you. Flyway runs as a dedicated startup phase before anything serves traffic. It applies only the application’s own SQL.
  • Proof against a real database. The generated integration test boots the whole application against a PostgreSQL container and lets it apply its own migrations.

By the end of this page you will have generated that application, run its Docker-backed integration test, and seen the three things the application itself owns: its migrations, its database connection configuration, and its repository’s use of the transaction seam.

  • JDK 21
  • Apache Maven
  • A reachable Docker daemon: mvn verify runs a real integration test that starts a PostgreSQL container for the application to migrate and connect against.

Run this command from an empty directory of your choice. It creates a new rest-postgresql-app project inside it. VERTIQUE_VERSION is declared once and reused for both the archetype version and the generated project’s vertiqueVersion property, matching Quickstart’s convention.

Terminal window
VERTIQUE_VERSION=0.3.0
mvn -B -ntp archetype:generate \
-DarchetypeGroupId=dev.vertique \
-DarchetypeArtifactId=vertique-archetype-rest-postgresql \
-DarchetypeVersion=$VERTIQUE_VERSION \
-DgroupId=com.example \
-DartifactId=rest-postgresql-app \
-Dversion=0.1.0-SNAPSHOT \
-Dpackage=com.example.restpostgresqlapp \
-DvertiqueVersion=$VERTIQUE_VERSION \
-DinteractiveMode=false

Expected result: BUILD SUCCESS, and a new rest-postgresql-app directory containing:

rest-postgresql-app/AGENTS.md
rest-postgresql-app/README.md
rest-postgresql-app/config/application.json
rest-postgresql-app/pom.xml
rest-postgresql-app/src/main/java/com/example/restpostgresqlapp/AppComponent.java
rest-postgresql-app/src/main/java/com/example/restpostgresqlapp/AppModule.java
rest-postgresql-app/src/main/java/com/example/restpostgresqlapp/package-info.java
rest-postgresql-app/src/main/java/com/example/restpostgresqlapp/model/CreateItemRequest.java
rest-postgresql-app/src/main/java/com/example/restpostgresqlapp/model/Item.java
rest-postgresql-app/src/main/java/com/example/restpostgresqlapp/model/UpdateItemRequest.java
rest-postgresql-app/src/main/java/com/example/restpostgresqlapp/model/package-info.java
rest-postgresql-app/src/main/java/com/example/restpostgresqlapp/repository/ItemRepository.java
rest-postgresql-app/src/main/java/com/example/restpostgresqlapp/repository/package-info.java
rest-postgresql-app/src/main/java/com/example/restpostgresqlapp/resource/ItemResource.java
rest-postgresql-app/src/main/java/com/example/restpostgresqlapp/resource/package-info.java
rest-postgresql-app/src/main/resources/db/migration/V1__create_items.sql
rest-postgresql-app/src/test/java/com/example/restpostgresqlapp/ApplicationIT.java

Its pom.xml declares only vertique-starter-rest, vertique-starter-postgresql, and vertique-launcher in production scope. The project composes two independent starters side by side and does not use one combined persistence-and-REST starter. See Application model for how starters compose. Test scope adds vertique-application-test, vertique-db-test (which brings Testcontainers), junit-jupiter, and rest-assured.

Working directory: rest-postgresql-app/.

Terminal window
cd rest-postgresql-app
mvn -ntp verify

Expected result: BUILD SUCCESS, with the test summary reporting Tests run: 2, Failures: 0, Errors: 0, Skipped: 0. supportsItemCrud starts a real PostgreSQL container, lets the application apply its own migrations against it, and then drives one ordered item CRUD journey over HTTP. bootstrapReadsWorkingDirectoryConfig checks that startup configuration is read from the project’s config/ directory. See Test database lifecycle below for how the container and the application fit together.

The starter wires migration execution; the application owns the migration content. The generated AppComponent names PostgresqlPersistenceModule, the aggregate that vertique-starter-postgresql publishes, alongside the REST application starter:

@VertiqueApp
@Singleton
@Component(
modules = {
RestApplicationModule.class,
PostgresqlPersistenceModule.class,
AppModule.class,
GeneratedJaxRsResourcesModule.class
})
interface AppComponent extends VertiqueApplicationComponent {}

PostgresqlPersistenceModule contributes a MIGRATE-phase ApplicationStartupStep that runs before the INFRA, SERVICES, and EDGE deployment phases, so no manual migrationRunner().migrate(vertx) call is needed. It migrates only the application’s own SQL, read from classpath:db/migration by default. The generated src/main/resources/db/migration/V1__create_items.sql is the whole schema this archetype ships:

CREATE TABLE items (
id UUID PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT
);

Add V2__*.sql (and later) files to the same directory to evolve the schema. Flyway applies every unapplied migration, in version order, the next time the MIGRATE phase runs.

ItemRepository extends PgSqlRepository (from vertique-db-postgresql) and is constructed with an injected connection pool and exception mapper. It never opens a connection itself:

@Singleton
public final class ItemRepository extends PgSqlRepository {
@Inject
public ItemRepository(Pool pool, PgDbExceptionMapper exceptionMapper) {
super(pool, exceptionMapper);
}
public Future<Item> create(String name, String description) {
UUID id = UUID.randomUUID();
return this.<Item>query("INSERT INTO items (id, name, description) VALUES ($1, $2, $3)"
+ " RETURNING id, name, description")
.params(Tuple.of(id, name, description))
.mapping(Item::fromRow)
.returning();
}
}

findById, update, and delete follow the same shape: each calls the inherited fluent query(String) builder once and lets PgDbExceptionMapper translate any database failure into a typed DataAccessException before it propagates. See vertique-db-core’s module reference for the full Query/PagedQuery/OffsetPagedQuery builder surface this repository is built on.

Every method on the generated ItemRepository runs a single statement, so none of them need a transaction. The pooled connection each query(...) call borrows is enough on its own. The canonical seam for work that must span more than one statement is SqlRepository.transaction(), documented by vertique-db-core’s module reference. It returns a TransactionBuilder whose execute(...) runs a function against a transactional connection. The builder commits automatically on success, rolls back automatically on failure, and applies the same exception translation that query(...) gives you:

repository.transaction()
.serializable()
.readOnly()
.execute(conn -> queryWork(conn));

Reach for this when a repository method needs to read-then-write, or write to more than one table, as a single atomic unit. The single-statement CRUD this archetype ships does not need it.

ApplicationIT boots the whole application against a real PostgreSQL container. The container is declared and started in a static initializer, and the application’s config is built from it, before the VertiqueAppExtension field is constructed. The database is therefore reachable by the time the extension boots the application and its MIGRATE-phase Flyway step connects:

static final PostgresContainer db = new PostgresContainer().withDatabaseName("app_db");
static {
db.start();
}
static final JsonObject CONFIG = buildConfig();
@RegisterExtension
static final VertiqueAppExtension app = VertiqueAppExtension.forFactory(new AppComponentVertiqueComponentFactory())
.withConfig(CONFIG);

buildConfig() points the db section at the container’s own mapped connection and sets flyway.mode to MIGRATE explicitly, so the application applies the migration and the container does not:

private static JsonObject buildConfig() {
DbPoolConfig pool = db.toPoolConfig();
return new JsonObject()
.put("http", new JsonObject().put("port", 0))
.put("management", new JsonObject().put("enabled", true).put("port", 0))
.put(
"db",
new JsonObject()
.put("host", pool.host())
.put("port", pool.port())
.put("database", pool.database())
.put("user", pool.user())
.put("password", pool.password()))
.put("flyway", new JsonObject().put("mode", "MIGRATE"));
}

A reachable Docker daemon is required: the static initializer’s db.start() propagates any discovery or startup failure, so a missing daemon fails the build and does not silently skip the proof. See vertique-db-test’s module reference for PostgresContainer’s shared-server isolation model and the timeouts that bound container startup, application boot, and the test itself.

Vertique resolves the config/ directory against the running process’s own working directory (see Configuration). When you run mvn -ntp exec:java from rest-postgresql-app/, the application reads the generated rest-postgresql-app/config/application.json:

{
"http": { "port": 8080 },
"management": { "port": 9090, "enabled": true },
"db": {
"host": "localhost",
"port": 5432,
"database": "vertique",
"user": "vertique",
"password": "vertique"
},
"flyway": { "mode": "MIGRATE" }
}

Change the db values to match your database. An application started without connection configuration, for example from another working directory, fails fast at the migration startup step (Either flyway.jdbcUrl or db.host + db.database must be configured for Flyway migrations).

flyway.mode defaults to MIGRATE. The file sets it explicitly for clarity. The vertique-db-flyway module reference documents the other modes.

Start a disposable PostgreSQL matching the values above:

Terminal window
docker run -d --name rest-postgresql-app-db \
-e POSTGRES_DB=vertique -e POSTGRES_USER=vertique -e POSTGRES_PASSWORD=vertique \
-p 5432:5432 postgres:16-alpine

Working directory: rest-postgresql-app/, with config/application.json from the previous section in place.

Terminal window
mvn -ntp exec:java

Expected result: log lines showing Flyway migrating schema public to version 1 - create items, followed by Succeeded in deploying verticle. Leave this terminal open. The application keeps running in the foreground until stopped.

In a separate terminal, with the application still running:

Terminal window
curl -s -i -X POST http://localhost:8080/items \
-H 'Content-Type: application/json' \
-d '{"name":"Widget","description":"A sample widget"}'

Expected result: a 201 Created status line, a Location: /items/{id} response header, and a JSON body carrying a server-generated id, for example:

HTTP/1.1 201 Created
Cache-Control: no-store
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Location: /items/d781c3a3-2ec6-4bc3-a1ac-d2a4f5f8b50a
Content-Type: application/json
content-length: 93
X-Request-Id: 09bb7eb0-48d3-4720-aea2-05f85af016b6
{"id":"d781c3a3-2ec6-4bc3-a1ac-d2a4f5f8b50a","name":"Widget","description":"A sample widget"}

Substitute the id your own request returned into the read call:

Terminal window
curl -s -i http://localhost:8080/items/d781c3a3-2ec6-4bc3-a1ac-d2a4f5f8b50a

Expected result: a 200 OK status line and the same JSON body the create call returned.

An empty, whitespace-only, or over-length name, and a malformed identifier, are all rejected before either reaches the database. See REST APIs for the request-validation gate that guards this and every other generated resource.

Return to the terminal running mvn -ntp exec:java and press Ctrl+C. Then remove the disposable database:

Terminal window
docker rm -f rest-postgresql-app-db