Here are folder hierarchies I’d recommend (and why). I’ll give you two “battle-tested” layouts: one for Flyway (SQL-first) and one for Liquibase (master + includes). Both are designed to minimize merge conflicts, support multi-env, and keep things discoverable.
Flyway folder hierarchy (recommended for most teams)
db/
migration/
V20260120_1015__create_users_table.sql
V20260120_1040__add_email_verified_column.sql
V20260120_1200__backfill_email_verified.sql
V20260121_0900__idx_users_email_verified.sql
callbacks/
beforeMigrate.sql
afterMigrate.sql
afterEachMigrate.sql
repeatable/
R__views.sql
R__functions.sql
R__permissions.sql
Rules
- One change = one migration (schema, data backfill, index).
- Use timestamp versions to avoid collisions:
VYYYYMMDD_HHMMSS__desc.sql. - Put “non-versioned but re-applied” things in repeatables:
- views, functions, grants (stuff you want to converge to the latest definition).
- Keep callbacks separate and tiny.
When you have multiple schemas
db/
migration/
public/
V...
billing/
V...
When you have multiple DBs (multi-tenant but separate databases)
- Keep the same scripts; orchestration decides which DBs to run on.
- Don’t duplicate migrations per tenant.
Liquibase hierarchy (master changelog + per-change files)
db/
changelog/
db.changelog-master.yaml
changes/
2026/
01/
2026-01-20_001_create_users_table.yaml
2026-01-20_002_add_email_verified.yaml
2026-01-20_003_backfill_email_verified.yaml
2026-01-21_001_idx_users_email_verified.yaml
repeatable/
views/
users_view.yaml
functions/
normalize_phone.yaml
testdata/
2026-01-20_seed_minimal.yaml
Master file is boring on purpose
- It only includes other files (ideally whole directories).
- You want people creating new files under
changes/..., not editing one big file.
Changeset conventions
- Unique
id:2026-01-20-001-create-users author: team/service or username- Prefer
logicalFilePathstability if you move files.
Multi-service monorepo layout (common in real orgs)
services/
payments/
db/
migration/ (or changelog/)
customers/
db/
migration/
shared/
db/
repeatable/ (optional shared views/functions only if truly shared)
Rule: migrations are owned by the service that owns the schema. Shared DB objects are usually a smell unless you’ve intentionally centralized schema ownership.
Environment-specific migrations (handle carefully)
Avoid “prod-only migrations” if you can. If you must:
- Liquibase: use contexts/labels, but don’t hide required schema behind them.
- Flyway: separate locations, e.g.
db/migration+db/migration-prod, and keep it rare.
Prefer: same migrations everywhere; differences should be configuration, not schema.
Naming standards (what prevents chaos)
- Prefix ordering:
YYYY-MM-DD_###_... - Short, verb-first description:
add_,create_,drop_,backfill_,idx_ - Keep the file name close to what it does:
...__idx_users_email_verified.sqlnot...__misc.sql
My “interview answer” (tight)
“I structure migrations so each change is a separate file, ordered deterministically by timestamp to avoid collisions. For Flyway I keep versioned migrations in
db/migration, repeatables for views/functions/grants, and optional callbacks. For Liquibase I keep a minimal master changelog that includes achanges/YYYY/MMfolder, so merge conflicts become adding files, not editing shared ones.”