PostgreSQL through plain SQL, optional, migrated before a deploy
The API reaches PostgreSQL with plain SQL through JdbcClient, starts and serves everything else without a database, and migrates it in a deploy step, never during a cold start.
Context
ADR 0005 chose PostgreSQL on Neon. The experiments that come next are about what a database does under load: connection pools, isolation and locks, indexes, an outbox. The site's own data (visits, introductions) comes after them. Two constraints shape how the API holds the database: Cloud Run scales the API to zero, so every cold start is a visitor waiting (ADR 0009); and Neon suspends its compute when idle, so the first query after a pause wakes it.
Decision
- Plain SQL.
JdbcClientwith the SQL written out, not Spring Data repositories as ADR 0005 had it: the experiments show what statements, transactions and locks do, which an ORM exists to hide. - Optional. Spring Boot's DataSource auto-configuration stops the application when it has no URL, so it is
excluded and imported again by
DatabaseAutoConfigurationonly when there is a database to connect to: aspring.datasource.url, or connection details from Docker Compose (bootRun) or Testcontainers (tests). Features reach it through theDatabaseport; without one it is a Null Object whose every use answers 503DATABASE_NOT_CONFIGURED. A database that cannot be reached answers 503DATABASE_UNAVAILABLE./api/healthanswers for the API alone. - A lazy, small pool. No connection at startup and none kept while idle, so a cold start never waits for Neon and Neon can suspend between visits; five connections at most for the API's own queries. The experiments build pools of their own.
- Migrations before a deploy. Flyway migrates at startup where the database is local (development, tests). In
production it is off at startup; a Cloud Run job runs the new image with the
migrateprofile, which applies the migrations and exits, before the new revision takes traffic, and a failed migration stops the deploy, not the site. - A schema for the experiments. Their tables live in
lab, apart from the site's own data, so an experiment can be reset or loaded until it breaks without touching what visitors left. - Every endpoint that reaches the database is rate-limited. Each request can wake Neon, and its free plan budgets compute hours.
Consequences
- Tests need Docker:
./gradlew checkstarts PostgreSQL 17 with Testcontainers, the same major version as production and the Compose file. - Measured on one vCPU, six starts each: 3.6 s before and 3.8 s after the database arrived, within the noise of the machine; the production service opens no connection until the first query.
- The first query after Neon has suspended pays for waking it. Neon runs on AWS and Azure, not Google Cloud, so the
Cloud Run region should be chosen next to Neon's: Frankfurt (
europe-west3besideaws-eu-central-1) keeps a round trip short; the current default,europe-west1, is a few milliseconds further away. - A connection string in production comes from Secret Manager, never from the repository; without one the API runs with its database features answering 503.