todos table with a Flyway migration and serve create, read, update, and delete requests from a Spring Data JPA REST controller. Every connection uses TLS with full certificate verification.
Prerequisites
- JDK 17 or later, as required by Spring Boot 4. This guide was tested with Java 25, Spring Boot 4.1.1, pgjdbc 42.7.13, Hibernate 7.4.5, HikariCP 7.0.2, and Flyway 12.4.0.
- A ClickHouse Cloud account
psql, to create the database. You can also run theCREATE DATABASEstatement in the SQL console.
Create a ClickHouse Managed Postgres service
In the ClickHouse Cloud console, click New service and select Postgres. The instance is ready in a few minutes. See the quickstart for a walkthrough.Get your connection details
Click Connect in the left sidebar of your service. The modal shows your username, password, server, and port, and it has a Directly / via PgBouncer toggle. With Directly selected, turn on Use SSL and open the jdbc tab. It shows a JDBC URL that you can use in Spring Boot after two changes, which you make in Configure the connection: the database name and the name of the CA certificate file.5432. A Spring Boot app is a long-lived process that keeps its own HikariCP pool, 10 connections by default, so it doesn’t need a second pooler in front of Postgres. Keep the number of app instances multiplied by the pool size below the service’s max_connections. If you run many instances, see Use PgBouncer.
In the modal, click Download CA certificate. You can also download it from Settings → CA Certificate. The certificate is unique to your instance, so the driver can use it to verify that it’s talking to your server.
Set up the project
Generate a Maven project with Spring Initializr and the Spring Web, Spring Data JPA, Flyway Migration, and PostgreSQL Driver dependencies:Configure the connection
Move the CA certificate you downloaded into the project directory and rename it toca-certificate.pem.
Create a database for the app. Replace <PASSWORD> and the host with the values from the Connect modal:
src/main/resources/application.properties with the following. In an existing app, add the spring.datasource and spring.jpa lines, and remove any other spring.datasource settings, such as an H2 URL. The URL is the one from the jdbc tab, with the database changed to guide_spring and sslrootcert changed to the renamed file:
src/main/resources/application.properties
sslmode=verify-full and sslrootcert from the URL. It loads every certificate in the CA file and checks both the certificate chain and the hostname, so you don’t need any extra TLS code. Spring Boot reads the password from the DATABASE_PASSWORD environment variable, so it stays out of the file.
The other settings:
ddl-auto=validatemakes Hibernate check at startup that your entities match the tables that Flyway creates. Hibernate doesn’t change the schema.open-in-view=falsereturns each connection to the pool when its transaction ends, instead of holding it until the HTTP response is written.
A relative
sslrootcert path is resolved against the working directory of the JVM. ./mvnw spring-boot:run starts the app in the project directory, so ca-certificate.pem works there. When you run the packaged JAR elsewhere, use an absolute path. pgjdbc doesn’t expand ~.If you copy the URL with Use SSL turned off, it has no sslmode, and pgjdbc then uses prefer: it encrypts the connection but doesn’t verify the server. With the wrong CA, the connection fails with PKIX path building failed. If the file is missing, it fails with Could not open SSL root certificate file. If the host doesn’t match the certificate, for example when you connect by IP address, it fails with The hostname ... could not be verified by hostnameverifier PgjdbcHostnameVerifier.Create the table with Flyway
Flyway runs the SQL files insrc/main/resources/db/migration in version order when the app starts, before Hibernate validates the schema. Create src/main/resources/db/migration/V1__create_todos.sql:
src/main/resources/db/migration/V1__create_todos.sql
flyway_schema_history table and only runs new files on later starts. To change the schema, add a new file such as V2__add_due_date.sql instead of editing one that has already run.
An app that used H2 usually let Hibernate create its tables, because
ddl-auto defaults to create-drop for embedded databases. For Postgres, the default is none, so add a migration that creates your existing tables. An @Id with a plain @GeneratedValue also needs a sequence named after the entity, such as employee_seq for Employee, that increases by 50, Hibernate’s default allocation size. Otherwise, startup fails with Schema validation: missing sequence [employee_seq], or with The increment size of the [employee_seq] sequence is set to [50] in the entity mapping but the mapped database sequence increment size is [1] if the sequence increases by 1:Query the database
Create the entity insrc/main/java/com/example/todos/Todo.java. Spring Boot maps the createdAt field to the created_at column:
src/main/java/com/example/todos/Todo.java
src/main/java/com/example/todos/TodoRepository.java. Spring Data implements the interface at runtime:
src/main/java/com/example/todos/TodoRepository.java
src/main/java/com/example/todos/TodoController.java, with one repository call per route:
src/main/java/com/example/todos/TodoController.java
@Transactional.
Run and verify
Set the password and start the app:POST requests return the new rows, and the PATCH request returns the updated row. -w '\n' adds a line break after each response:
pg_stat_ssl while the app is running:
guide_spring and then public, and click the todos table. The flyway_schema_history table next to it holds the migration history. The todos table contains the remaining row:
Use PgBouncer
When many app instances together approach the Postgres connection limit, connect the app through the bundled PgBouncer on port6432, and keep Flyway on the direct connection. In application.properties, change the port in spring.datasource.url to 6432, and add the spring.flyway lines below it:
src/main/resources/application.properties
spring.flyway.url is set, Flyway opens its own connection and doesn’t use the spring.datasource username and password, so set spring.flyway.user and spring.flyway.password too. The same CA certificate works for both ports.
PgBouncer runs in transaction pooling mode, so each transaction can run on a different Postgres connection. With Spring Boot:
- Prepared statements work with the default driver settings, so you don’t need
prepareThreshold=0. From the fifth execution of a statement on a connection (prepareThreshold), pgjdbc uses a named server-side prepared statement. It frees statements with a protocol-levelClosemessage, which the bundled PgBouncer handles. - pgjdbc 42.7.5 or later is required. Older versions send the
extra_float_digitsstartup parameter, and PgBouncer rejects the connection withFATAL: unsupported startup parameter: extra_float_digits. Spring Boot 4 includes a newer pgjdbc. On older Spring Boot versions, set<postgresql.version>42.7.13</postgresql.version>in the<properties>section ofpom.xml. - Session settings don’t persist.
SETstatements andConnection.setSchema, which HikariCP’sspring.datasource.hikari.schemaproperty uses, apply only to the Postgres connection that runs them, and they can leak to other clients. ThecurrentSchemaandoptionsURL parameters fail withFATAL: unsupported startup parameter. Use schema-qualified table names, orSET LOCALinside a transaction. - Flyway works through PgBouncer by default, because it takes a transaction-level advisory lock. Migrations that run
CREATE INDEX CONCURRENTLYneedspring.flyway.postgresql.transactional-lock=false, which switches Flyway to a session-level lock. Through PgBouncer, that lock fails withUnable to release PostgreSQL advisory lockand stays held on a pooled connection, so use the direct connection for Flyway. - Pool size: each HikariCP connection is a cheap client connection to PgBouncer. PgBouncer limits the Postgres connections it opens for each database and user (
default_pool_size), no matter how many client connections your app instances open. Transactions over that limit wait in PgBouncer for up toquery_wait_timeout(120 seconds by default) instead of failing in HikariCP. A larger HikariCP pool adds concurrency only up to that limit, so the default of 10 per instance is a good starting point. See PgBouncer and Postgres connection limits.
pg_stat_ssl reports the connection between PgBouncer and Postgres, so the TLS query in Run and verify doesn’t show the app’s connections through PgBouncer. The app’s connection to PgBouncer is still verified: a wrong CA certificate fails in the same way.
Next steps
- Connection: connection strings, PgBouncer, and TLS
- Settings: Postgres and PgBouncer parameters, such as
max_connections - Read replicas: send read-only queries to a replica
- Security: IP access lists and private networking