Database Migrations: Flyway vs Liquibase vs Atlas

Compare database migration tools Flyway, Liquibase, and Atlas. Learn migration strategies, rollback patterns, CI/CD integration, and schema drift detection.

Y
Yash Pritwani
13 min read

Database Migrations Done Right

Schema migrations are one of the most dangerous operations in software. A bad migration can corrupt data, cause downtime, or create irreversible damage. Yet many teams still apply schema changes manually or with ad-hoc scripts.

PrimaryRead + WriteReplica 1Replica 2WAL streamWAL streamRead-onlyRead-only

Database replication: the primary handles writes while replicas serve read queries via WAL streaming.

A proper migration tool provides:

  • Version-controlled schema changes
  • Repeatable, idempotent migrations
  • Rollback capability
  • CI/CD integration
  • Schema drift detection

Flyway: The SQL-First Approach

Flyway uses plain SQL files with a naming convention. It is simple, predictable, and SQL-native.

db/migration/
├── V1__create_users_table.sql
├── V2__add_email_to_users.sql
├── V3__create_orders_table.sql
├── V4__add_index_on_orders_user_id.sql
└── V5__add_status_to_orders.sql
-- V1__create_users_table.sql
CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- V2__add_email_to_users.sql
ALTER TABLE users ADD COLUMN email VARCHAR(255) UNIQUE;
CREATE INDEX idx_users_email ON users (email);

-- V3__create_orders_table.sql
CREATE TABLE orders (
    id BIGSERIAL PRIMARY KEY,
    user_id BIGINT NOT NULL REFERENCES users(id),
    total DECIMAL(10, 2) NOT NULL,
    status VARCHAR(50) DEFAULT 'pending',
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

Get more insights on DevOps

Join 2,000+ engineers who get our weekly deep-dives. No spam, unsubscribe anytime.

# Run migrations
flyway -url=jdbc:postgresql://localhost:5432/myapp \
       -user=postgres \
       -password=secret \
       migrate

# Check current version
flyway info

# Validate migrations match what was applied
flyway validate

Flyway with Docker Compose (run migrations before app starts):

services:
  flyway:
    image: flyway/flyway:10
    command: migrate
    volumes:
      - ./db/migration:/flyway/sql
    environment:
      FLYWAY_URL: jdbc:postgresql://postgres:5432/myapp
      FLYWAY_USER: postgres
      FLYWAY_PASSWORD: secret
    depends_on:
      postgres:
        condition: service_healthy

  app:
    image: my-app:latest
    depends_on:
      flyway:
        condition: service_completed_successfully

Liquibase: The Changelog Approach

Liquibase uses changelog files (XML, YAML, JSON, or SQL) that describe changes abstractly. This abstraction allows database-agnostic migrations.

# changelog.yaml
databaseChangeLog:
  - changeSet:
      id: 1
      author: yash
      changes:
        - createTable:
            tableName: users
            columns:
              - column:
                  name: id
                  type: bigint
                  autoIncrement: true
                  constraints:
                    primaryKey: true
              - column:
                  name: name
                  type: varchar(255)
                  constraints:
                    nullable: false
              - column:
                  name: created_at
                  type: timestamp with time zone
                  defaultValueComputed: NOW()

  - changeSet:
      id: 2
      author: yash
      changes:
        - addColumn:
            tableName: users
            columns:
              - column:
                  name: email
                  type: varchar(255)
                  constraints:
                    unique: true
        - createIndex:
            tableName: users
            indexName: idx_users_email
            columns:
              - column:
                  name: email
      rollback:
        - dropIndex:
            tableName: users
            indexName: idx_users_email
        - dropColumn:
            tableName: users
            columnName: email
# Run migrations
liquibase --url=jdbc:postgresql://localhost:5432/myapp \
          --username=postgres \
          --password=secret \
          --changelog-file=changelog.yaml \
          update

# Generate rollback SQL
liquibase rollback-sql --count=1

# Diff between database and changelog
liquibase diff

Atlas: The Declarative Approach

Atlas by Ariga takes a different approach. Instead of writing migration scripts, you declare the desired schema and Atlas computes the diff.

# schema.hcl - Declare desired state
schema "public" {}

table "users" {
  schema = schema.public
  column "id" {
    type = bigserial
  }
  column "name" {
    type = varchar(255)
    null = false
  }
  column "email" {
    type = varchar(255)
    null = true
  }
  column "created_at" {
    type    = timestamptz
    default = sql("NOW()")
  }
  primary_key {
    columns = [column.id]
  }
  index "idx_users_email" {
    columns = [column.email]
    unique  = true
  }
}

table "orders" {
  schema = schema.public
  column "id" {
    type = bigserial
  }
  column "user_id" {
    type = bigint
    null = false
  }
  column "total" {
    type = decimal(10, 2)
    null = false
  }
  column "status" {
    type    = varchar(50)
    default = "pending"
  }
  primary_key {
    columns = [column.id]
  }
  foreign_key "fk_orders_user" {
    columns     = [column.user_id]
    ref_columns = [table.users.column.id]
  }
}
# Inspect current database schema
atlas schema inspect -u "postgresql://postgres:secret@localhost:5432/myapp?sslmode=disable"

# Compute diff between desired and actual
atlas schema diff \
  --from "postgresql://postgres:secret@localhost:5432/myapp?sslmode=disable" \
  --to "file://schema.hcl"

# Apply changes (Atlas generates migration SQL automatically)
atlas schema apply \
  -u "postgresql://postgres:secret@localhost:5432/myapp?sslmode=disable" \
  --to "file://schema.hcl"

# Or generate versioned migration files
atlas migrate diff add_orders_table \
  --dir "file://migrations" \
  --to "file://schema.hcl" \
  --dev-url "docker://postgres/16/dev"
Cloud$5,000/moMigrateBare MetalDocker + LXC$200/mo96% cost reduction

Cloud to self-hosted migration can dramatically reduce infrastructure costs while maintaining full control.

Comparison

Feature Flyway Liquibase Atlas
Approach Imperative (SQL scripts) Imperative (changelogs) Declarative (desired state)
Language SQL XML/YAML/JSON/SQL HCL/SQL
Rollback Manual (undo scripts) Built-in (per changeset) Computed (auto-diff)
DB-agnostic No (SQL is DB-specific) Yes (abstract types) Partial (HCL is generic)
Schema drift detection validate command diff command schema diff command
CI/CD integration CLI, Docker, Maven CLI, Docker, Maven CLI, Docker, GitHub Action
Schema visualization No No Yes (atlas schema inspect)
Dev database Manual setup Manual setup Auto (docker://)
Price Free (Community), Paid (Teams) Free (OSS), Paid (Pro) Free (OSS), Paid (Pro)
Learning curve Low Medium Medium
Maturity Very mature Very mature Newer (growing fast)

Safe Migration Patterns

Regardless of which tool you use, follow these patterns for zero-downtime migrations:

1. Expand-and-Contract

Free Resource

CI/CD Pipeline Blueprint

Our battle-tested pipeline template covering build, test, security scan, staging, and zero-downtime deployment stages.

Get the Blueprint

Never rename or remove columns directly. Instead:

-- Step 1: Add new column (expand)
ALTER TABLE users ADD COLUMN full_name VARCHAR(255);

-- Step 2: Backfill data
UPDATE users SET full_name = name;

-- Step 3: Deploy app code that reads from both, writes to both

-- Step 4: Deploy app code that reads from full_name only

-- Step 5: Drop old column (contract) -- separate migration, days later
ALTER TABLE users DROP COLUMN name;

2. Never Lock Large Tables

-- BAD: Locks the entire table while adding default
ALTER TABLE orders ADD COLUMN priority INTEGER DEFAULT 0;

-- GOOD: Add column nullable first, then backfill
ALTER TABLE orders ADD COLUMN priority INTEGER;
-- Backfill in batches
UPDATE orders SET priority = 0 WHERE id BETWEEN 1 AND 10000;
UPDATE orders SET priority = 0 WHERE id BETWEEN 10001 AND 20000;
-- Then add default for new rows
ALTER TABLE orders ALTER COLUMN priority SET DEFAULT 0;
CodeBuildTestDeployLiveContinuous Integration / Continuous Deployment Pipeline

A typical CI/CD pipeline: code flows through build, test, and deploy stages automatically.

3. Create Indexes Concurrently

-- BAD: Blocks writes
CREATE INDEX idx_orders_status ON orders (status);

-- GOOD: Non-blocking (PostgreSQL)
CREATE INDEX CONCURRENTLY idx_orders_status ON orders (status);

At TechSaaS, we use Flyway for most projects because SQL-first means no abstraction layer between you and the database. The naming convention is intuitive, and the Docker integration makes it trivial to run migrations as part of a compose stack. For teams that want a declarative approach, Atlas is impressive and growing fast.

#database#migrations#flyway#liquibase#atlas#postgresql

Related Service

Platform Engineering

From CI/CD pipelines to service meshes, we create golden paths for your developers.

Need help with devops?

TechSaaS provides expert consulting and managed services for cloud infrastructure, DevOps, and AI/ML operations.

We Will Build You a Demo Site — For Free

Like it? Pay us. Do not like it? Walk away, zero complaints. You will spend way less than hiring developers or any agency.

47+ companies trusted us
99.99% uptime
< 48hr response

No spam. No contracts. Just a free demo.