Database Migrations: Converting Legacy IDs to UUIDs Without Downtime

    May 6, 2024
    10 min read
    Long read
    Tutorial
    uuid
    migration
    database
    best-practices

    Migrating from legacy sequential IDs (INT, BIGINT) to UUIDs can feel like defusing a bomb — especially in a live production environment.

    But here’s the good news: with careful planning and a gradual migration strategy, you can make the switch without downtime, data loss, or user disruption.

    Let’s walk through a proven, zero-downtime migration plan — step-by-step.


    🎯 Why Migrate to UUIDs?

    Reasons for switching to UUIDs:

    • Enable global uniqueness across services
    • Prevent ID collisions in distributed systems
    • Avoid leaking business information (e.g., order counts via auto-increment)
    • Support future sharding or multi-region replication

    But you don't want to:

    • Lock your tables
    • Break APIs
    • Lose referential integrity

    So here's how to migrate safely.


    🧱 Step 1: Add a UUID Column

    Add a new uuid column to your existing table, nullable at first.

    sql
    ALTER TABLE users ADD COLUMN uuid UUID;

    Don’t drop your legacy id column — yet.

    If your database supports it, generate UUIDs automatically for new rows:

    PostgreSQL

    sql
    ALTER TABLE users ALTER COLUMN uuid SET DEFAULT gen_random_uuid();

    > Make sure the pgcrypto or uuid-ossp extension is enabled.

    MySQL (8.0+)

    sql
    ALTER TABLE users ADD COLUMN uuid BINARY(16) DEFAULT (UUID_TO_BIN(UUID()));

    🔁 Step 2: Backfill Existing UUIDs

    Populate UUIDs for existing rows.

    In small batches:

    sql
    UPDATE users
    SET uuid = gen_random_uuid()
    WHERE uuid IS NULL
    LIMIT 1000;

    Repeat until every record has a UUID. You can script this with your application or a background worker.


    ✍️ Step 3: Write to Both Columns

    Update your application to write both id and uuid for new records.

    In your ORM:

    ruby
    user = User.create!(name: "Jane Doe", uuid: SecureRandom.uuid)

    In code, you can fallback to generating UUID if not supplied by DB:

    python
    import uuid
    record = db.insert({"uuid": uuid.uuid4(), "name": "Alice"})

    ✅ At this point, all new and existing records have UUIDs — but your app still relies on the legacy id.


    🕵️ Step 4: Dual Reads and Joins

    Update your app to support reading by either ID:

    • API: allow both /users/123 and /users/uuid-abc...
    • DB: join using uuid for newer services, id for older ones

    You may need database views or helper functions for backward compatibility.


    🔐 Step 5: Update Foreign Keys and Relationships

    This is where many migrations get stuck. You must repeat the same steps for all related tables:

    1. Add uuid columns to referencing tables

    2. Backfill based on JOIN with legacy ID

    3. Add foreign key constraints (optional during transition)

    Example:

    sql
    ALTER TABLE orders ADD COLUMN user_uuid UUID;
    
    UPDATE orders
    SET user_uuid = users.uuid
    FROM users
    WHERE orders.user_id = users.id;

    Once all joins and lookups are UUID-compatible, you're ready to cut over.


    🔄 Step 6: Flip the Primary Key

    Once:

    • All new writes use UUIDs
    • All reads and joins work on UUIDs
    • All foreign keys are migrated

    You can flip your schema.

    sql
    ALTER TABLE users DROP CONSTRAINT users_pkey;
    ALTER TABLE users ADD PRIMARY KEY (uuid);

    Optionally drop the legacy ID:

    sql
    ALTER TABLE users DROP COLUMN id;

    Or keep it around if legacy support is still required.


    🚀 Step 7: Celebrate (Then Clean Up)

    You've now migrated to UUIDs with:

    • No downtime
    • No broken joins
    • No lost data

    Next steps:

    • Remove any old id-based API routes
    • Normalize internal tooling to use UUIDs
    • Monitor for unexpected joins or fallback behavior

    🧠 Bonus Tips

    • Use uuidv7 or ULID for ordered UUIDs with better index performance
    • Compress UUIDs using UUID_TO_BIN() in MySQL for smaller indexes
    • Keep dual write code in place during a soft launch — it’s great for rollback
    • Don’t forget to update test fixtures and seed data

    Final Thoughts

    Migrating from legacy IDs to UUIDs doesn’t have to be scary.

    By breaking it into safe, incremental steps, you can move at your own pace — without locking tables or waking up at 3am.

    UUIDs offer a scalable, secure, and modern foundation for your systems. Migrating to them just requires a little care, a lot of coffee, and a solid plan.

    📦 Migration: complete. Downtime: zero.

    Generate Your Own UUIDs

    Ready to put this knowledge into practice? Try our UUID generators:

    Generate a Single UUID

    Create a UUID with our fast, secure generator

    Bulk UUID Generator

    Need multiple UUIDs? Generate them in bulk

    Summary

    This article provides a zero-downtime, step-by-step strategy for migrating legacy auto-increment IDs to UUIDs in live production databases. It covers schema evolution, dual writes, backfills, and cutover techniques with minimal risk.

    TLDR;

    Migrating from sequential IDs to UUIDs in production requires care — but it's completely doable without downtime.

    Key steps:

    • Add UUID columns alongside existing IDs (dual schema)
    • Backfill UUIDs for existing records
    • Enable dual writes and queries using both IDs
    • Flip primary keys during a controlled switchover

    The trick is to evolve the schema gradually, not in one giant leap.

    Cookie Consent

    We use cookies to enhance your experience on our website. By accepting, you agree to the use of cookies in accordance with our Privacy Policy.