Schema migrations
Two schema modes
The same service binary supports:
- schema_mode=migrate — apply its embedded ordered migrations;
- schema_mode=none — issue no DDL and use a pre-provisioned schema.
Application behavior does not branch on the mode.
Embedded migrations
//go:embed migrations/*.sql
var Migrations embed.FS
bootstrap.Migrations(
Migrations,
"migrations",
"schema_migrations_widget",
)
Bootstrap runs migrations before opening the application pool. Use a service-specific history table when multiple services share one database server.
Expand and contract
Rolling deployments overlap versions:
- Expand with nullable/additive schema.
- Deploy code that reads both forms and writes the new form.
- Backfill in bounded, observable batches.
- Switch reads after evidence is complete.
- Remove the old form in a later release.
Avoid a single migration that rewrites a large table under an exclusive lock.
Migration rules
- Only the owning service changes its schema.
- Every change is a reviewed file, never an interactive production ALTER.
- Use explicit transactions only when the database operation supports them.
- Set lock and statement timeouts.
- Separate schema expansion from large data backfills.
- Make restart behavior and partial failure clear.
- A rollback plan may be “forward fix”; do not promise impossible DDL reversal.
Exercise
Plan adding a required normalized_name field to a large widgets table. Write expansion, dual-write, backfill, verification, read-switch, and cleanup stages with abort criteria.
Check yourself
- What does schema_mode=none guarantee?
- Why do migrations run before repositories receive a pool?
- What makes a migration safe for rolling deploys?
- Who owns the backup and restore plan?