Skip to main content
Create your own

Zero-Downtime Deployment with Expand-Contract Pattern

Hello! Welcome to your next lesson on designing high-load distributed systems.

In our previous lesson, we explored PostgreSQL partitioning as a powerful technique for managing very large tables. Now, we address the next logical challenge: how do you modify the schema of such a table—one that might be serving tens of thousands of requests per second—without taking your service offline?

Your work on high-availability payment platforms and trading systems has undoubtedly highlighted the business-critical need for continuous operation. A simple ALTER TABLE that locks a core table for even a few minutes is not an option. This lesson introduces a foundational pattern to solve this exact problem.

Our learning outcome is to analyze the stages of the expand-contract pattern, detailing the state of application code and database schema at each step to ensure zero-downtime deployment. This methodical approach is a cornerstone of modern database management and continuous delivery in high-load environments.

1. The Challenge: Why Naive Schema Changes Fail

In a development environment, changing a table schema seems simple. For instance, to rename a column, you might run:

ALTER TABLE users RENAME COLUMN username TO user_login;

However, on a production database with a high-traffic table, this operation typically acquires an ACCESS EXCLUSIVE lock. This is the most restrictive lock level in PostgreSQL, blocking all operations, including SELECT queries. For a system like a payment gateway, this translates directly to downtime, failed transactions, and a poor user experience.

To evolve our systems safely, we need a strategy that decouples the schema change from the application deployment and avoids these disruptive locks.

2. Introducing the Expand-Contract Pattern

The expand-contract pattern (also known as parallel change) is a systematic, multi-stage process for making backward-compatible changes to a system's interface—in our case, the database schema. It ensures that at every step, the system remains fully functional.

To get a high-level overview, please start with this introductory reading.

Using the expand and contract pattern | Prisma's Data Guide

This guide from Prisma's Data Guide provides a clear definition of the expand-contract pattern and its purpose in achieving zero-downtime migrations.

Please read the section 'What is the expand and contract pattern?'. Focus on its definition as a multi-step process and its key benefits: avoiding downtime and enabling rollbacks.

The core idea is to break the migration into three main phases:

  1. Expand: Additively introduce the new schema elements (columns, tables) alongside the old ones. The system "expands" to support both structures simultaneously.
  2. Migrate: Transition the application and data to use the new structure. This phase is often the most complex, involving data backfilling and careful changes to application logic.
  3. Contract: Once the old structure is no longer used, it is safely removed. The system "contracts" back to a single, consistent state.

This visual diagram provides an excellent summary of the entire flow, which we will now break down in detail.

The expand-migrate-contract pattern for zero-downtime schema changes. This diagram illustrates the key steps, showing the coordinated changes in both application code and the database schema over time.

3. A Detailed Analysis of the Stages

The power of this pattern lies in its deliberate, step-by-step nature, which requires careful coordination between database changes and application deployments. Let's analyze the state of the system at each stage for a common use case: renaming column_A to column_B.

Stage 1: Expand the Schema and Start Dual Writing

The first phase involves preparing the database and the application to handle both the old and new data structures.

  • Database Change:

    • Add the new column (column_B) to the table.
    • Crucially, the new column must be NULL-able or have a DEFAULT value. This allows PostgreSQL to add the column without rewriting the entire table, making the ALTER TABLE operation a near-instantaneous metadata change that doesn't require a heavy lock.
    ALTER TABLE my_table ADD COLUMN column_B <datatype> NULL;
    
  • Application Change (Deployment 1):

    • Writes: Modify the application code so that any INSERT or UPDATE operation writes the relevant data to both column_A and column_B.
    • Reads: The application continues to read exclusively from column_A.

At the end of this stage, column_A remains the source of truth for reads, but all new and updated data is being mirrored to column_B, ensuring it stays current going forward.

Using the expand and contract pattern | Prisma's Data Guide

This resource details the initial phases of the pattern: adding the new schema and updating the application to perform dual writes.

Please read 'Step 1: Build and deploy the new schema' and 'Step 2: Expand the interface'. Note the importance of making new columns nullable and the logic of dual writing.

Stage 2: Backfill Existing Data

Now that new data is being dual-written, we need to handle the historical data.

  • Data Migration (Background Process):
    • Run a script to copy data from column_A to column_B for all existing rows where column_B is NULL.
    • For large tables, this must be done in small, manageable batches to avoid long-running transactions, excessive I/O, and replication lag. A typical approach is to update a few thousand rows at a time in a loop with a small delay.
    -- Run this repeatedly in a script until it updates 0 rows
    UPDATE my_table
    SET column_B = column_A
    WHERE id IN (
        SELECT id FROM my_table
        WHERE column_B IS NULL AND column_A IS NOT NULL
        LIMIT 1000
    );
    
  • Application State: The application continues to run as before (reading from A, writing to A and B). This ensures that any records updated by users during the backfill process are correctly handled by the application's dual-write logic.

Using the expand and contract pattern | Prisma's Data Guide

This part of the guide covers the critical data migration step, which runs in the background while the application is live.

Now, read 'Step 3: Migrate existing data to the new schema'. Given your experience, consider the operational challenges of running such a backfill on a multi-terabyte table in a live production environment.

At the end of this stage, both column_A and column_B contain identical data. You can verify this with queries that check for discrepancies.

Stage 3: Switch Reads to the New Column

With the data fully migrated and consistent, we can now switch the application to use the new column as its source of truth.

  • Application Change (Deployment 2):
    • Reads: Modify the application code to read from column_B instead of column_A.
    • Writes: The application continues to write to both columns. This is a safety measure; if a critical bug is found after deployment, you can quickly roll back to the previous application version without data loss, as column_A is still being maintained.

After this deployment is verified as stable in production, you can proceed to the next step. This is often controlled by a feature flag for maximum safety.

Stage 4: Stop Writing to the Old Column (Contract Phase Begins)

Now we begin to "contract" the system by phasing out the old structure.

  • Application Change (Deployment 3 or Feature Flag Flip):
    • Writes: Modify the application to stop writing to column_A. It now reads from and writes to only column_B.
    • Reads: Continue reading from column_B.

At this point, column_A is no longer being updated and its data becomes stale. The migration is functionally complete from the application's perspective.

Using the expand and contract pattern | Prisma's Data Guide

These steps describe the cutover, where the application logic is switched to use the new schema. The example in the full text also mentions using feature flags, a common practice for this kind of controlled rollout.

Read 'Step 4: Test the new interface', 'Step 5: Cut reads over to the new interface', and 'Step 6: Discontinue writing to the original structure'.

Stage 5: Clean Up

The final step is to remove the old, unused elements from the system.

  • Application Change (Optional, good practice):
    • Remove any code that references column_A. This can be done in a follow-up cleanup task.
  • Database Change:
    • Drop the old column. This is another fast, non-blocking metadata operation.
    ALTER TABLE my_table DROP COLUMN column_A;
    

This GitHub discussion provides a concise, practical summary of the pattern framed in terms of deployments, which should resonate with your role as an Engineering Manager.

Implementation Guidance for Expand and Contract Pattern ...

This discussion frames the pattern in terms of 'Change Sets', each corresponding to a deployment, which is a very practical way to think about execution.

Skim through the initial post. Notice how the author breaks the process down into three distinct deployments. This reinforces the phased nature of the pattern and the tight coupling between code and schema changes.

Activity: Mapping the System States

To solidify your understanding, let's map the state of the system at each major step.

Stage DB Schema State Application Read Target Application Write Target(s)
Initial column_A exists column_A column_A
1. Expand column_A, column_B (nullable) column_A column_A, column_B
2. Backfill column_A, column_B (data being copied) column_A column_A, column_B
3. Switch Read column_A, column_B (fully populated) column_B column_A, column_B
4. Stop Old Write column_A (stale), column_B column_B column_B
5. Contract column_B only column_B column_B

This table clearly shows how the source of truth for reads and the targets for writes evolve, ensuring a valid data source is always available to the application.

Conclusion

In this lesson, we have dissected the expand-contract pattern, a critical strategy for zero-downtime schema evolution in high-load systems.

Key Takeaways:

  • Avoids Downtime: The pattern's primary goal is to avoid taking ACCESS EXCLUSIVE locks on live tables, ensuring continuous availability.
  • Coordinated Changes: Successful execution requires careful coordination across multiple deployments, involving both database schema alterations and application code updates.
  • Phased & Reversible: The process is broken into distinct phases (Expand, Migrate, Contract). Most steps are reversible, providing a safety net if issues arise.
  • Key Enablers: The process relies on backward-compatible changes, such as adding nullable columns, dual writing, and using feature flags for controlled cutovers.

Preview of the Next Lesson

Theory is essential, but practice is where mastery is built. In our next lesson, "Implement a zero-downtime column addition using the expand-contract pattern," we will move from analysis to implementation. You will apply the stages we've discussed today to a practical exercise in PostgreSQL, solidifying your understanding of the mechanics at each step.

Can't find a good explanation? Sign up and we'll make it for you

Sign up