Changelog Organization: Master Files, include/includeAll, Directory Structures
A single changelog file works fine for a tutorial. It stops working the moment two developers add migrations on the same day, or when you need to find the changeset that introduced a column added six months ago, or when a feature branch’s migrations must be reviewed independently before merging.
Changelog organization is not a polish step — it is what determines whether Liquibase stays manageable at scale or turns into a source of merge conflicts and mystery failures. This article covers the tools Liquibase gives you (include, includeAll, master changelogs) and the structures that work in real projects.
The Master Changelog Pattern
The master changelog is the single file you point Liquibase at — the one referenced by liquibase.properties or spring.liquibase.change-log. It contains no changesets itself. Its only job is to reference other files.
# db/changelog/db.changelog-master.yaml
databaseChangeLog:
- includeAll:
path: db/changelog/migrations/
relativeToChangelogFile: false
This separation matters for two reasons:
- Single entry point. The command
liquibase update(or Spring Boot startup) always points at one file. That file never changes structure — only the migration files it pulls in grow over time. - Reviewability. In a code review, you can open
db.changelog-master.yamland immediately understand the shape of the migration system without reading individual changesets.
include vs includeAll
Liquibase gives you two ways to pull in external changelog files.
include — explicit, ordered
include references one specific file:
databaseChangeLog:
- include:
file: db/changelog/migrations/2026/05/001-create-users-table.yaml
relativeToChangelogFile: false
- include:
file: db/changelog/migrations/2026/05/002-create-products-table.yaml
relativeToChangelogFile: false
Use include when:
- You need to control the exact execution order that alphabetical sorting cannot guarantee
- You are including a special file from a different path (e.g., a shared library of stored procedures)
- You want to explicitly exclude certain files from a directory without building a custom filter
Drawback: Every new migration file must be manually added to the master changelog. In a team of five developers, that means five simultaneous edits to the same master file — merge conflicts on every PR.
includeAll — automatic, alphabetical
includeAll picks up every changelog file in a directory (and its subdirectories) in alphabetical order:
databaseChangeLog:
- includeAll:
path: db/changelog/migrations/
relativeToChangelogFile: false
Use includeAll when:
- Multiple developers are adding migration files simultaneously
- You want new files to be picked up automatically without touching the master changelog
- Your naming convention guarantees correct alphabetical ordering
The critical dependency: includeAll order is alphabetical. Your file naming convention must encode execution order. If it does not, a file named add-column.yaml will always run before create-table.yaml, regardless of when it was created.
relativeToChangelogFile
This attribute changes how Liquibase resolves the path:
# Absolute path from the classpath root
- includeAll:
path: db/changelog/migrations/
relativeToChangelogFile: false # default
# Path relative to the master changelog file's location
- includeAll:
path: migrations/
relativeToChangelogFile: true
relativeToChangelogFile: true is useful when the master changelog is not at the classpath root — it lets you move the whole changelog directory without updating paths. For Spring Boot projects where the master changelog is at src/main/resources/db/changelog/db.changelog-master.yaml, both forms work; relative paths are more portable.
includeAll with a filter
By default, includeAll picks up all .yaml, .xml, .json, and .sql files. You can restrict which files are included by writing a class that implements IncludeAllFilter:
package com.example.liquibase;
import liquibase.changelog.IncludeAllFilter;
public class ExcludeTestDataFilter implements IncludeAllFilter {
@Override
public boolean include(String changeLogPath) {
// Skip files named *-test-data.yaml
return !changeLogPath.contains("-test-data");
}
}
Reference it in the master changelog:
- includeAll:
path: db/changelog/migrations/
filter: com.example.liquibase.ExcludeTestDataFilter
In practice, filtering by context (Article 10) is cleaner than a custom filter — but the filter is useful when you need to physically separate files that should never run together.
Naming Conventions
The naming convention for changelog files is the foundation of includeAll ordering. Get it wrong and you’ll spend an afternoon debugging why a column migration ran before its table was created.
Recommended: timestamp or sequence prefix
Option A — Date + sequence (recommended for teams):
YYYYMMDD-NNN-description.yaml
20260503-001-create-users-table.yaml
20260503-002-create-products-table.yaml
20260510-001-add-users-phone-column.yaml
The date prefix groups by when the migration was written. The sequence number handles multiple migrations on the same day. This format:
- Sorts correctly alphabetically
- Makes
git log -- db/changelog/migrations/readable - Allows two developers to write migrations on the same day without colliding (different sequence numbers)
Option B — Global sequence (simpler, riskier):
0001-create-users-table.yaml
0002-create-products-table.yaml
0003-add-users-phone-column.yaml
Simpler, but two developers both writing “the next migration” will grab the same sequence number. Requires coordination or a centralized sequence registry.
Option C — Release + sequence (release-based teams):
v1.0.0-001-create-users-table.yaml
v1.0.0-002-create-products-table.yaml
v1.1.0-001-add-users-phone-column.yaml
Groups migrations by release. Useful when your changelog needs to align with versioned software releases.
Changeset ID convention
The changeset id inside the file should match the file prefix to make tracing easy:
# File: 20260503-001-create-users-table.yaml
- changeSet:
id: "20260503-001"
author: abhay
The combination of id + author + filename is the unique key in DATABASECHANGELOG. Using the date-prefix as the ID means the tracking table row immediately tells you when the migration was written.
Directory Structure Patterns
There is no single correct structure. The right choice depends on team size, release cadence, and whether the database is shared across multiple applications. Here are the four most common patterns.
Pattern 1: Date-Partitioned (Recommended for most teams)
db/changelog/
├── db.changelog-master.yaml
└── migrations/
└── 2026/
├── 05/
│ ├── 20260503-001-create-users-table.yaml
│ ├── 20260503-002-create-products-table.yaml
│ └── 20260510-001-add-users-phone-column.yaml
└── 06/
└── 20260601-001-create-orders-table.yaml
Master changelog:
databaseChangeLog:
- includeAll:
path: db/changelog/migrations/
relativeToChangelogFile: false
includeAll recurses into subdirectories, so the 2026/05/ partitioning is automatic.
Best for: Most teams. Files are naturally grouped by time, git log on the directory gives a clean history, and merge conflicts are rare because developers work in different months most of the time.
Drawback: Finding all migrations for a specific feature requires git log --grep rather than browsing a directory.
Pattern 2: Release-Partitioned
db/changelog/
├── db.changelog-master.yaml
└── releases/
├── v1.0.0/
│ ├── 001-create-users-table.yaml
│ └── 002-create-products-table.yaml
└── v1.1.0/
├── 001-add-users-phone-column.yaml
└── 002-create-orders-table.yaml
Master changelog (explicit include per release directory):
databaseChangeLog:
- includeAll:
path: db/changelog/releases/v1.0.0/
relativeToChangelogFile: false
- includeAll:
path: db/changelog/releases/v1.1.0/
relativeToChangelogFile: false
Note that each release directory is an explicit includeAll rather than a recursive scan of releases/. This prevents a new release directory from being picked up before its migrations are ready — you add the includeAll line when the release is ready to merge.
Best for: Teams with formal release trains, where migrations must be reviewed as a release unit.
Drawback: The master changelog must be updated when a new release directory is added — a single-line change, but still a touch point for merge conflicts.
Pattern 3: Object-Type Partitioned
db/changelog/
├── db.changelog-master.yaml
└── schema/
├── tables/
│ ├── 001-users.yaml
│ └── 002-products.yaml
├── indexes/
│ ├── 001-users-indexes.yaml
│ └── 002-products-indexes.yaml
├── views/
│ └── 001-user-order-summary.yaml
└── data/
└── 001-seed-categories.yaml
Master changelog (explicit order by object type):
databaseChangeLog:
- includeAll:
path: db/changelog/schema/tables/
relativeToChangelogFile: false
- includeAll:
path: db/changelog/schema/indexes/
relativeToChangelogFile: false
- includeAll:
path: db/changelog/schema/views/
relativeToChangelogFile: false
- includeAll:
path: db/changelog/schema/data/
relativeToChangelogFile: false
The master changelog enforces object-type ordering: tables before indexes before views before data. This guarantees that a view referencing a table always runs after the table is created.
Best for: Database-heavy projects managed by a DBA team where organization by database object type is more natural than by date or release.
Drawback: As the project grows, individual type directories get large. Mixing “when was this created” with “what type is this” is harder to reason about in git log.
Pattern 4: Feature-Branch Staging Area (Advanced)
This pattern is for teams where feature branches live for a long time and migrations on multiple branches must be integrated without conflict.
db/changelog/
├── db.changelog-master.yaml
├── migrations/ ← merged, stable migrations
│ └── 2026/
│ └── 05/
│ └── 20260503-001-create-users-table.yaml
└── pending/ ← feature migrations awaiting merge
├── feature-payments/
│ └── 001-create-payments-table.yaml
└── feature-coupons/
└── 001-create-coupons-table.yaml
Master changelog:
databaseChangeLog:
# Stable migrations — always run
- includeAll:
path: db/changelog/migrations/
relativeToChangelogFile: false
# Pending feature migrations — included during development, removed on merge
- includeAll:
path: db/changelog/pending/
relativeToChangelogFile: false
When a feature branch merges to main, its migration files are moved from pending/feature-name/ to migrations/YYYY/MM/ with the correct date prefix. The pending/ directory entry in the master changelog can stay as-is — an empty directory with no files produces no error.
Best for: Teams where feature branches run independent environments and need isolated migration sets. Uncommon — most teams find Pattern 1 sufficient.
Recommended Structure for This Series
The e-commerce tutorial uses Pattern 1 (date-partitioned) because it scales to any team size and produces the most readable git history.
src/main/resources/
└── db/
└── changelog/
├── db.changelog-master.yaml
└── migrations/
└── 2026/
├── 05/
│ ├── 20260503-001-create-users-table.yaml
│ ├── 20260503-002-create-products-table.yaml
│ └── 20260503-003-create-product-categories-table.yaml
└── 06/
└── 20260601-001-create-orders-table.yaml
# db.changelog-master.yaml — never changes structure
databaseChangeLog:
- includeAll:
path: db/changelog/migrations/
relativeToChangelogFile: false
With this setup, adding a new migration is exactly one action: create a new file with the correct date prefix. The master changelog, CI pipeline, and Spring Boot configuration never need to change.
Splitting One Large File Into Multiple
If you have inherited a project with a single enormous changelog file, here is how to split it without breaking existing databases.
The wrong way: Delete the original file and create many smaller files. Liquibase will try to re-apply every changeset (because the FILENAME column in DATABASECHANGELOG changed) and fail on every one.
The right way:
- Keep the original file in place and add it as an explicit
includeat the top of the master changelog - Create new files for future changesets
- Optionally run
changelogSyncon new environments to baseline them
# db.changelog-master.yaml during migration
databaseChangeLog:
# Legacy monolith file — do not rename or move
- include:
file: db/changelog/legacy/db.changelog-v1.yaml
relativeToChangelogFile: false
# New structured migrations
- includeAll:
path: db/changelog/migrations/
relativeToChangelogFile: false
The FILENAME stored in DATABASECHANGELOG is the path as Liquibase saw it when the changeset first ran. Renaming or moving the file changes that path, and Liquibase considers the changeset untracked (and tries to re-run it). Never rename or move a file that has changesets already applied to any environment.
Common Mistakes
Naming files without a sortable prefix: Files named create-users.yaml, add-column.yaml, create-orders.yaml sort alphabetically in an unexpected order. add-column.yaml runs before create-orders.yaml which runs before create-users.yaml — but add-column references the users table that hasn’t been created yet. Alphabetical ordering is rigid; your names must encode the intended order.
Touching the master changelog on every PR: If the master changelog uses explicit include lines, every developer adding a migration also edits the master changelog. This creates merge conflicts on every PR. Switch to includeAll — then the master changelog is never touched after initial setup.
Moving a file after it has been applied to any database: Liquibase stores the full file path in DATABASECHANGELOG. Moving migrations/001-create-users.yaml to migrations/2026/05/001-create-users.yaml changes the stored path. On the next update, Liquibase sees the file as new and tries to re-apply it — failing because the users table already exists. Treat applied changelog file paths as immutable, exactly like changeset IDs.
Best Practices
- Master changelog contains no changesets — only
include/includeAllreferences; keep it structurally stable - Use
includeAlloverincludein most cases — it eliminates master changelog merge conflicts - Date-prefix all filenames —
YYYYMMDD-NNN-description.yamlguarantees alphabetical = chronological - Match changeset ID to filename prefix —
id: "20260503-001"makesDATABASECHANGELOGrows self-documenting - Never rename or move an applied file — the stored path in
DATABASECHANGELOGis as immutable as the changeset ID - Recurse subdirectories with one
includeAll— date-partitioned month folders are picked up automatically; no master changelog update needed when a new month starts
What You’ve Learned
- The master changelog is a structural file — it contains no changesets, only references to other files
includeis explicit and ordered;includeAllis automatic and alphabetical — choose based on team workflowrelativeToChangelogFile: truemakes paths portable when the master changelog is not at the classpath root- File naming convention is the load-bearing element of
includeAllordering — date-prefix guarantees chronological execution - Four directory structures cover most real projects: date-partitioned, release-partitioned, object-type, and feature-staged
- Moving or renaming an applied changelog file breaks
DATABASECHANGELOGtracking — treat file paths as immutable
Next: Article 8 — Common Migration Patterns: 12 Real-World Schema Changes with MySQL — the practical changesets you’ll write over and over: adding columns, renaming, creating foreign keys, adding indexes, changing types, and more.