Mahamudul Hasan Rubel
HomeAboutProjectsSkillsExperienceBlogPhotosContact
Mahamudul Hasan Rubel

Senior Software Engineer crafting high-performance web applications and SaaS platforms.

Navigation

  • Home
  • About
  • Projects
  • Skills
  • Experience
  • Blog
  • Photos
  • Contact

Get in Touch

Available for senior/lead roles and consulting.

bd.mhrubel@gmail.comHire Me

© 2026 Mahamudul Hasan Rubel. All rights reserved.

Built with using Next.js 16 & Tailwind v4

Back to Blog
LaravelPHPJune 23, 20264 min read

Laravel Migrations for Blue-Green Deployments: A Practical Guide

Master Laravel migrations for blue-green deployment success. Learn the expand-and-contract pattern to ensure zero-downtime database schema evolution.

LaravelPHPDatabaseMigrationsDevOpsBlue-Green DeploymentBackend

Last month, a simple RENAME COLUMN migration brought our production traffic to a crawl because the old application code still expected the original column name. We fixed it with a hotfix, but it reminded me that database schema evolution is the most dangerous part of any deployment.

If you’re running a high-traffic Laravel app, you can't just run php artisan migrate and hope for the best. You need a strategy that keeps your database compatible with two versions of your code simultaneously. That’s where the expand-and-contract pattern comes in.

The Problem with Standard Laravel Migrations

By default, Laravel migrations are destructive. When you rename a column or drop a table, the current running instance of your application—the "blue" version—often breaks immediately if it relies on that schema. In a blue-green deployment, your database must support both the old code and the new "green" code during the transition period.

We initially tried wrapping everything in a single migration file. It failed because our CI/CD pipeline ran the migration before the new code was fully active. We needed a way to decouple the database change from the application logic.

Implementing the Expand-and-Contract Pattern

The core idea is to never perform a breaking change in a single step. Instead, we break the migration into three distinct phases: Expand, Migrate, and Contract.

1. Expand (The Additive Phase)

Instead of renaming a column, you add a new one. Your database now supports both the old code (using the old column) and the new code (using the new column).

PHP
Schema::table('users', function (Blueprint $table) {
    $table->string('full_name')->nullable(); #6A9955">// New column
});

2. Migrate (The Data Sync Phase)

Once the new column exists, you need to sync the data. I usually use a background job or a dedicated command for this. Running heavy data migrations inside a standard migration file is a recipe for timeouts, especially if you have millions of rows.

PHP
#6A9955">// Run this via a separate Artisan command
User::chunk(1000, function ($users) {
    foreach ($users as $user) {
        $user->update(['full_name' => $user->first_name . ' ' . $user->last_name]);
    }
});

3. Contract (The Cleanup Phase)

Only after the new version is deployed and verified do you remove the old column. This is the "contract" step. It’s the safest way to finalize your database schema evolution.

Deterministic Database Schema Evolution

To keep your migrations deterministic, avoid relying on the state of the database. Hard-code your expectations. If you are adding a column, ensure it’s nullable or has a default value so existing code doesn't crash when it attempts to insert records.

I’ve found that using DB::statement for complex changes is sometimes cleaner than the Schema builder. For instance, creating a view to bridge the gap between old and new columns can keep your app running smoothly while you transition.

If you’re managing complex multi-tenant environments, this pattern becomes even more critical. You might want to look at how to handle WordPress plugin architecture for zero-downtime database migrations as a reference for how to manage these transitions across different plugin versions.

Why Blue-Green Deployments Matter

Blue-green deployments allow you to switch traffic instantly. However, if your database schema isn't backward compatible, the "switch" is just a trigger for a production incident. By ensuring that your Laravel migrations are always additive during the transition, you remove the "big bang" risk.

Here is a quick checklist for your next deployment:

  1. Additive only: Can the old code still read/write to the database?
  2. Nullable defaults: If adding a column, are you handling existing rows?
  3. Rollback plan: Can you revert the code without reverting the database schema?

What I’d Do Differently Next Time

Honestly, I’m still refining how we handle data synchronization. Running a manual command to sync columns is fine for a few thousand rows, but for our larger tables, we’ve started using database triggers for real-time synchronization. It’s more complex to set up, but it ensures that the "expand" phase never lags behind the application state.

Also, don't forget that if you are using Implementing Laravel Multi-Tenancy with PostgreSQL Schemas, you have to repeat these steps across every tenant schema. Automation is not optional at that scale.

Are you worried about migration locks? Use DB::statement('SET lock_timeout = "5s"') at the start of your migrations. It’s better to fail the migration than to lock your entire production users table for twenty minutes.

Frequently Asked Questions

Q: Should I use Schema::rename() in production? A: Never. It locks the table for the duration of the rename. Use the expand-and-contract pattern instead.

Q: How do I handle large datasets during the sync phase? A: Use chunking in your jobs and add a small usleep() between chunks to keep the database replication lag under control.

Q: Is this overkill for small projects? A: Probably. If you have a maintenance window or low traffic, standard migrations are fine. But if you’re aiming for 99.99% uptime, this is the cost of doing business.

Managing database changes is an exercise in patience. Don't rush the contract phase—leave the old column around for at least one full deployment cycle. You’ll thank yourself when you need to roll back.

Back to Blog

Similar Posts

LaravelPHPJune 22, 20263 min read

Laravel Horizon Auto-scaling: Custom Prometheus Metrics for KEDA

Learn to build a custom Prometheus exporter for Laravel Horizon to enable precise KEDA auto-scaling on Kubernetes, moving beyond basic resource limits.

Read more
LaravelPHP
June 22, 2026
4 min read

Laravel Read-Write Splitting: Deterministic Connection Routing Guide

Master Laravel read-write splitting with deterministic connection routing. Scale your database performance and high availability without complex external proxies.

Read more
LaravelPHPJune 22, 20264 min read

Laravel Database Sharding: Implementing Deterministic Horizontal Partitioning

Laravel database sharding allows you to scale beyond single-instance limits. Learn how to implement horizontal partitioning using custom connection resolvers.

Read more