Introduction
Managing database changes in an evolving application can be challenging — especially when your schema and database objects keep growing over time. Without a well-defined strategy, developers often spend more time fixing deployment issues than writing actual business logic.
In this post, I’ll share our Liquibase-based deployment strategy designed to help developers focus on coding while ensuring database changes are consistent, versioned, and easy to deploy.
The Challenge
With Liquibase, every schema change is defined in a changeset.
Over time, teams accumulate dozens or even hundreds of DDL files, often with no clear naming or organization. This makes it difficult to know:
- Which file is the latest version of a table.
- Whether changes have already been deployed.
- How to maintain dependency order (tables before triggers, etc.).
In our case, the original folder structure looked like this:
❌ Before (unstructured)
db-schema/
├── TICKET-101/
│ ├── create_employees.sql # creates employees table
│ ├── alter_projects.sql # adds columns to projects
│ └── fix_constraints.sql # quick constraint fixes
├── TICKET-102/
│ ├── create_departments.sql # creates departments table
│ ├── update_employees.sql # modifies employees from TICKET-101
│ └── projects_more_changes.sql # more changes to projects from TICKET-101
└── TICKET-103/
├── add_index_to_projects.sql # indexes on projects (depends on prior alters)
└── employees_hotfix.sql # emergency tweak to employees (again from TICKET-101)
Why this is problematic
- Cross‑ticket edits with vague names:
update_employees.sql,projects_more_changes.sql,employees_hotfix.sqldon’t say what changed or in what order they must run. - No version indicators: You can’t tell which change is “latest” for
employeesorprojects. - Mixed concerns in one ticket: DDL for multiple objects sits together, obscuring dependencies.
- Deployment ordering risk: Applying
TICKET-103beforeTICKET-102could fail if required columns/indexes don’t exist yet.
Our Deployment Strategy
We separate database definitions into two main categories:
1. Tables
Includes:
- Table creation & modifications
- Column additions, changes, deletions
- Primary keys, foreign keys, and constraints
- Indexes & unique constraints
Principles:
- Idempotent: Safe to run multiple times (e.g., using
IF NOT EXISTS). - Versioned: Use incremental numbers after release.
Example naming:
employees.001.sql -- Initial table
employees.002.sql -- Add department_id column
projects.001.sql -- Initial table
2. Database Objects
Includes:
- Functions & stored procedures
- Trigger functions & triggers
- Views
- Other programmable objects
Principles:
- Always use
CREATE OR REPLACEwhere possible. - Use
runOnChange="true"so changes are always applied.
Example naming:
fn_calculate_employee_tenure.sql
trigger_employee_audit_log.sql
vw_employee_summary.sql
sp_monthly_report.sql
File Naming Conventions
Tables
table_name.XXX.sql
.001.sql: Original definition (modifiable until release)
.002.sql: First post-release change
.003.sql: Next change
Database Objects
objectType_objectName.sql
Prefixes:
fn– Functionvw– Viewtrigger– Triggersp– Stored Procedure
Liquibase Changelog Structure
Master changelog (domain_name.changelog.yml):
databaseChangeLog:
- include:
file: tables.changelog.yml
relativeToChangelogFile: true
- include:
file: database-objects.changelog.yml
relativeToChangelogFile: true
Tables changelog (tables.changelog.yml):
databaseChangeLog:
- changeSet:
id: <ticket ID>-<brief summary>
author: dev
runOnChange: false
changes:
- sqlFile:
path: tables/employees.001.sql
relativeToChangelogFile: true
Migration Best Practices
- Separate concerns: Tables vs objects.
- Respect dependency order: Tables first, dependent objects after.
- Group related changes: e.g., all audit triggers in one changeset.
- Rollback support: Always provide rollback scripts when possible.
- Test before release: Run in dev/staging first.
- Document complex logic: Use comments for maintainability.
Folder Structure
db-schema/
├── domain_name.changelog.yml
├── tables.changelog.yml
├── database-objects.changelog.yml
├── tables/
│ ├── latest/
│ │ ├── employees.sql
│ │ └── projects.sql
│ ├── employees.001.sql
│ ├── employees.002.sql
│ ├── projects.001.sql
└── database-objects/
├── fn_calculate_employee_tenure.sql
├── trigger_employee_audit_log.sql
├── vw_employee_summary.sql
└── sp_monthly_report.sql
Key Rules Recap
- One table per file.
.001files are modifiable until release.- Post-release changes require
.002,.003… - Use lowercase for field types, uppercase for SQL keywords.
- All changes go through Liquibase — no direct DB edits.
Conclusion
A clear Liquibase deployment strategy saves hours of developer time, reduces production issues, and ensures database changes are traceable and repeatable.
By following structured naming, changelog organization, and best practices, you’ll keep deployments smooth while letting developers focus on writing features — not firefighting migration issues.