ADR-0010: Schema changes only via versioned migrations; dev seeds in a profile-gated folder
- Status: Accepted
- Date: 2026-06-05
- Scope: core (any backend owning a relational schema)
Context
Letting the ORM mutate the schema (ddl-auto: update) makes the database’s
shape an emergent property of whatever code last booted — unreviewable,
unreproducible, and divergent between environments. Expenses ran this way early
on and the practice was abandoned; its leftover docs described a schema that no
longer existed. Separately, local development needs seed data (an admin user, a
demo account) that must never reach production.
Decision
- The ORM never touches the schema:
ddl-auto: validate. Hibernate (or
equivalent) only checks that entities match the database. - Every schema change is a versioned migration (Flyway
V*.sqlor
equivalent), reviewed in the same PR as the code that needs it. - An applied migration is immutable — once a
V*file has been committed or
run, its Flyway checksum is frozen; never edit it. Any later change (even
adding a single audit column) is a new, higher-numbered migration, not an
edit of the old one. Editing an already-applied file breaks checksum validation
on every environment that ran it. (This bit us: an audit column had to ship as a
newV2, not a tweak toV1.) - Glob the existing
V*files before creating a new one — the next number
is derived from what’s on disk, never guessed from memory. - A new lifecycle state or model change backfills existing rows in the same
migration, so historical data matches the new model
(0001-status-as-source-of-truth). - Dev-only seeds live in a separate migration folder, gated by profile —
numbered far above the real sequence so they can never collide. In Expenses:
db/dev-migration/V900__seed_dev_admin.sql, included via
spring.flyway.locationsonly whenprofile=dev. Production never sees the
folder;docker compose down -v+ restart recreates the seeds. - Dev seed scripts are idempotent (
INSERT ... ON CONFLICT DO NOTHING, or an
existence guard) so re-running them against an already-seeded database is safe
and never errors on duplicates.
Consequences
- The schema is reviewable history; any environment can be rebuilt from zero.
- Slightly more ceremony per change (write the migration, mind the numbering,
never edit an applied file) — the cost that makes the database trustworthy. - Dev convenience data is structurally incapable of leaking to production.
Related
- 0002-clean-architecture-and-use-cases — the architecture this serves
- 0001-status-as-source-of-truth — why model changes backfill
Amendment — 2026-09-10: the dev profile runs Flyway out of order
The dev-seed rule above has a consequence it never stated. A dev database has already
applied V900, so every real migration written afterwards (V5, V76, …) has a lower
version than one already applied. Flyway applies in order only, so validation fails at
startup and the new migration never runs.
- The dev profile sets
spring.flyway.out-of-order: true, next to the dev-seed
location. The base profile never does: production never seesV9xx, and its
migrations must always apply in order, so an out-of-order version there is a mistake to
catch, not accept. - Evidence: Expenses (
application-dev.yml) and dracomania (application-dev.yaml) both
carry the pair, each with a comment explaining it.