Skip to content
Go back

Database Migrations: Liquibase vs Flyway vs Atlas

By SumGuy 9 min read
Database Migrations: Liquibase vs Flyway vs Atlas
Contents

Your Database Broke. Now What?

Flyway wins for a single service with simple SQL-only migrations, and Atlas wins the moment you want schema-as-code with automatic diffs instead of hand-written rollback scripts.

You’re running a Postgres instance in your home lab. Someone, maybe you at 2 AM, runs a bad migration and half your schema is gone. You didn’t version it. You didn’t test it. You just hoped.

That’s what schema migration tools are for. They’re the diff-and-patch system for your database, except they work and don’t require you to manually track who changed what in a spreadsheet.

The big three are Liquibase, Flyway, and Atlas. They all solve the same problem differently. Pick the wrong one for your stack and you’ll be hand-rolling SQL in CI pipelines by next year.

Should You Pick Flyway, Liquibase, Or Atlas For Your Database?

FlywayLiquibaseAtlas
StyleImperative SQL filesSQL, YAML, or XML changesetsDeclarative HCL or SQL
RollbacksYou write them yourselfAuto-generated for most changesDiff-based, shown before apply
RuntimeJVM requiredJVM requiredSingle Go binary
Drift detectionNoYes, via status commandYes, via schema diff
Best fitSingle service, simple schemaMulti-service, compliance needsNew systems, schema-as-code

The Big Picture: Declarative vs Imperative

Before we dig into each tool, here’s the foundational split that’ll define everything about your choice.

Imperative migrations are SQL files with explicit CREATE TABLE / ALTER TABLE / DROP COLUMN statements. You write the exact steps. Flyway uses this.

Declarative migrations describe your desired final schema state”this table should exist with these columns”and the tool figures out the steps. Liquibase and Atlas lean declarative (though Liquibase supports both).

Imperative is simpler to understand at first. You write SQL like you always have. But you’re responsible for rollbacks, backwards compatibility, and not breaking production.

Declarative is more powerful. The tool can detect conflicts, generate safe migration paths, and handle complex multi-step refactors. But it’s also opinionated about schema design.

It’s manual vs automatic transmission, basically. Manual gives you control. Automatic handles the traffic for you.

Flyway: The Simple Kid

Flyway is the tool you reach for when you want to stop thinking about schema versioning yesterday.

You create SQL migration files with names like V1__Initial_schema.sql, V2__Add_users_table.sql, V3__Create_indexes.sql. Flyway scans a directory, executes them in order against your database, and tracks which have run in a flyway_schema_history table.

That’s it. No YAML. No DSL. Just SQL.

Flyway Example

-- V1__create_posts.sql
CREATE TABLE posts (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_posts_created_at ON posts(created_at);
-- V2__add_author_column.sql
ALTER TABLE posts ADD COLUMN author_id BIGINT NOT NULL DEFAULT 1;
ALTER TABLE posts ADD CONSTRAINT fk_posts_author
FOREIGN KEY (author_id) REFERENCES users(id);

Flyway runs V1, then V2, and marks each as complete. If you run it again, it skips them. If a new migration V3 shows up, Flyway detects it and runs only that.

Flyway in CI

#!/bin/bash
set -e
# Create database if not exists
psql -U postgres -tc "SELECT 1 FROM pg_database WHERE datname='myapp'" | grep -q 1 || \
psql -U postgres -c "CREATE DATABASE myapp"
# Run migrations
flyway -url=jdbc:postgresql://localhost:5432/myapp \
-user=postgres \
-password="$DB_PASSWORD" \
-locations=filesystem:./migrations \
migrate
echo "Migrations applied"

Pros

Cons

Liquibase: The Swiss Army Knife

Liquibase is the enterprise heavyweight. It supports both imperative (SQL) and declarative (YAML/XML) migrations, tracks schema state, detects drift, and can generate rollbacks automatically.

You define changes in YAML, XML, or SQL files (called changesets). Liquibase combines them into a single changelog, executes them in order, and tracks everything in databasechangelog and databasechangeloglock tables.

Liquibase Example (YAML)

databaseChangeLog:
- changeSet:
id: 1
author: sumguy
changes:
- createTable:
tableName: posts
columns:
- column:
name: id
type: BIGINT
autoIncrement: true
constraints:
primaryKey: true
- column:
name: title
type: VARCHAR(255)
constraints:
nullable: false
- column:
name: created_at
type: TIMESTAMP
defaultValue: CURRENT_TIMESTAMP
- createIndex:
indexName: idx_posts_created_at
tableName: posts
columns:
- column:
name: created_at
- changeSet:
id: 2
author: sumguy
changes:
- addColumn:
tableName: posts
columns:
- column:
name: author_id
type: BIGINT
constraints:
nullable: false
references: users(id)
foreignKeyName: fk_posts_author

Liquibase in CI

#!/bin/bash
set -e
# Run migrations
java -jar liquibase.jar \
--url=jdbc:postgresql://localhost:5432/myapp \
--username=postgres \
--password="$DB_PASSWORD" \
--changeLogFile=db/changelog/db.changelog-master.yaml \
update
echo "Liquibase applied all pending changesets"

Pros

Cons

Atlas: The New Hotness

Atlas is the newcomer (active development since 2021). It’s declarative-first, cloud-native, and built in Go (so it’s a single binary, no JVM, no runtime).

Instead of writing migrations, you define your desired schema in a declarative format (HCL or plain SQL). Atlas figures out the migration path, shows you a diff, and applies it safely.

This is a different mental model: you’re not writing migrations, you’re declaring schema state.

Atlas Example (HCL)

schema "public" {
comment = "The public schema"
}
table "posts" {
schema = schema.public
column "id" {
type = bigserial
null = false
}
column "title" {
type = varchar(255)
null = false
}
column "content" {
type = text
null = false
}
column "author_id" {
type = bigint
null = false
}
column "created_at" {
type = timestamp
default = sql("CURRENT_TIMESTAMP")
}
primary_key {
columns = [column.id]
}
foreign_key "author" {
columns = [column.author_id]
ref_columns = [table.users.column.id]
on_delete = SET_NULL
}
index "idx_posts_created_at" {
columns = [column.created_at]
}
}
table "users" {
schema = schema.public
column "id" {
type = bigserial
}
column "name" {
type = varchar(255)
}
primary_key {
columns = [column.id]
}
}

Atlas in CI

#!/bin/bash
set -e
# Detect the current state, plan migrations, show diff
atlas schema diff \
--to file://schema.hcl \
--from "postgres://postgres:$DB_PASSWORD@localhost:5432/myapp?sslmode=disable" \
--format template
# Apply the migrations
atlas schema apply \
--to file://schema.hcl \
--url "postgres://postgres:$DB_PASSWORD@localhost:5432/myapp?sslmode=disable"
echo "Atlas applied schema changes"

Pros

Cons

Which One for Your Home Lab?

Pick Flyway if:

Pick Liquibase if:

Pick Atlas if:

The Postgres Workflow: Testing Migrations Locally

Whichever tool you pick, here’s the pattern that’ll save your bacon:

#!/bin/bash
# Test migration against a local Postgres container
# Spin up a test DB
docker run --rm -d \
--name test-postgres \
-e POSTGRES_PASSWORD=test \
-p 5433:5432 \
postgres:16
sleep 3 # Wait for startup
# Backup production schema (for test data)
pg_dump -U postgres -h production-db.local myapp_schema > /tmp/schema_backup.sql
# Restore into test DB
psql -U postgres -h localhost -p 5433 -f /tmp/schema_backup.sql
# Run migrations (Flyway example)
flyway -url=jdbc:postgresql://localhost:5433/myapp \
-user=postgres \
-password=test \
-locations=filesystem:./migrations \
migrate
# Test that the app still works with new schema
npm run test:integration -- --db-url=postgresql://postgres:test@localhost:5433/myapp
# If tests pass, you're safe. Clean up:
docker stop test-postgres

This workflow, test locally first, then CI, then production, is non-negotiable. You’ll catch 95% of “oops I broke something” moments before they hit production.

The Real Talk

All three tools work. Flyway is the tortoise, Liquibase is the Swiss Army knife, Atlas is the race car. Your choice depends on:

  1. Team familiarity. What does your team already know?
  2. Stack complexity. Single app, or multi-service mess?
  3. Risk tolerance. Want simple and predictable, or smart and automated?

Start with Flyway if you’re unsure. It’s the safest bet. Graduate to Liquibase if you need enterprise features. Jump to Atlas if you’re building something new and want to avoid migration pain entirely.

And seriously: test every migration locally first. No exceptions. Your 2 AM self will thank you.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Previous Post
Postgres Replication: Streaming + Logical
Next Post
MariaDB vs MySQL in 2026

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts