Persistence
View MarkdownVertique 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
UniqueConstraintViolationExceptioninstead 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.
Prerequisites
Section titled “Prerequisites”- JDK 21
- Apache Maven
- A reachable Docker daemon:
mvn verifyruns a real integration test that starts a PostgreSQL container for the application to migrate and connect against.
Generate the application
Section titled “Generate the application”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.
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=falseExpected result: BUILD SUCCESS, and a new rest-postgresql-app directory containing:
rest-postgresql-app/AGENTS.mdrest-postgresql-app/README.mdrest-postgresql-app/config/application.jsonrest-postgresql-app/pom.xmlrest-postgresql-app/src/main/java/com/example/restpostgresqlapp/AppComponent.javarest-postgresql-app/src/main/java/com/example/restpostgresqlapp/AppModule.javarest-postgresql-app/src/main/java/com/example/restpostgresqlapp/package-info.javarest-postgresql-app/src/main/java/com/example/restpostgresqlapp/model/CreateItemRequest.javarest-postgresql-app/src/main/java/com/example/restpostgresqlapp/model/Item.javarest-postgresql-app/src/main/java/com/example/restpostgresqlapp/model/UpdateItemRequest.javarest-postgresql-app/src/main/java/com/example/restpostgresqlapp/model/package-info.javarest-postgresql-app/src/main/java/com/example/restpostgresqlapp/repository/ItemRepository.javarest-postgresql-app/src/main/java/com/example/restpostgresqlapp/repository/package-info.javarest-postgresql-app/src/main/java/com/example/restpostgresqlapp/resource/ItemResource.javarest-postgresql-app/src/main/java/com/example/restpostgresqlapp/resource/package-info.javarest-postgresql-app/src/main/resources/db/migration/V1__create_items.sqlrest-postgresql-app/src/test/java/com/example/restpostgresqlapp/ApplicationIT.javaIts 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.
Run the test suite
Section titled “Run the test suite”Working directory: rest-postgresql-app/.
cd rest-postgresql-appmvn -ntp verifyExpected 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.
Application-owned migrations
Section titled “Application-owned migrations”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.
Repository pattern
Section titled “Repository pattern”ItemRepository extends PgSqlRepository (from vertique-db-postgresql) and is constructed with
an injected connection pool and exception mapper. It never opens a connection itself:
@Singletonpublic 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.
Transactions
Section titled “Transactions”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.
Test database lifecycle
Section titled “Test database lifecycle”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();
@RegisterExtensionstatic 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.
Configure the database connection
Section titled “Configure the database connection”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.
Run the application
Section titled “Run the application”Start a disposable PostgreSQL matching the values above:
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-alpineWorking directory: rest-postgresql-app/, with config/application.json from the previous section
in place.
mvn -ntp exec:javaExpected 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.
Call the API
Section titled “Call the API”In a separate terminal, with the application still running:
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 CreatedCache-Control: no-storeX-Content-Type-Options: nosniffX-Frame-Options: DENYLocation: /items/d781c3a3-2ec6-4bc3-a1ac-d2a4f5f8b50aContent-Type: application/jsoncontent-length: 93X-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:
curl -s -i http://localhost:8080/items/d781c3a3-2ec6-4bc3-a1ac-d2a4f5f8b50aExpected 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.
Stop the application
Section titled “Stop the application”Return to the terminal running mvn -ntp exec:java and press Ctrl+C. Then remove the disposable
database:
docker rm -f rest-postgresql-app-dbLearn more
Section titled “Learn more”- Vertique REST/PostgreSQL application archetype: the archetype’s own reference, including which dependencies the generated project declares.
vertique-starter-postgresqlmodule reference: the three modulesPostgresqlPersistenceModulecomposes, and what applications still own.vertique-db-coremodule reference: the exception hierarchy, theQuery/PagedQuerybuilders, andSqlRepository.transaction().vertique-db-postgresqlmodule reference:PgSqlRepository,PgDbExceptionMapper, and the PostgreSQL connection pool.vertique-db-flywaymodule reference:FlywayMode,FlywayConfig, and theMIGRATE-phase startup step.vertique-db-testmodule reference:PostgresContainer,DatabaseExtension, and the shared-server isolation model.- Quickstart
- Application model
- Configuration
- REST APIs
- What is Vertique?
Continue reading
Section titled “Continue reading”- Previous: MCP
- Next: Messaging with Kafka