A Simple Workflow for Database Migrations in Git

You build a new feature on a branch. You add a database migration. It works perfectly on your machine. You push it up for review. While you wait another developer merges their own feature into the main branch. Their feature also had a migration.

When your pull request is finally approved you merge it. A few minutes later someone reports that the main branch is broken on the CI server. Or worse a developer pulls the latest code and their local database is now corrupted. Everyone grinds to a halt trying to figure out what went wrong.

This is a common story. The problem is not that two people wrote code at the same time. The problem is that database migrations are not like other code. They represent sequential changes to a shared state. When two developers create migrations in parallel their sequences inevitably clash.

Why Migration Conflicts Happen

Most web frameworks like Rails or Django generate migration files with a timestamp or a sequential number in the filename. For example:

# Rails migration file
20231026100000_add_billing_to_users.rb

# Another developer’s migration file
20231026100500_create_products_table.rb

When you work on a feature branch you might generate a file like 20231026100000_add_user_profile.rb. Meanwhile another developer on a different branch generates 20231026100100_add_teams.rb. When their branch is merged first their migration becomes part of the official history. Your migration now has a timestamp that is out of order or it might depend on a database state that has been changed by the other migration.

A simple Git merge might not even show a conflict. The files have different names. But when the application tries to run the migrations it fails because the sequence is wrong or a table it expects to exist does not exist.

The Wrong Way to Fix It

When this happens developers often resort to desperate measures. They might manually rename their migration file to give it a later timestamp. This can work but it is error prone and feels messy. Others might try a complex git rebase and attempt to solve merge conflicts inside the schema file which is a recipe for disaster.

The worst solution is when the team just accepts this as normal. They get used to a main branch that is frequently broken. They tell new developers to run rails db:reset every time they pull new code. This destroys any test data and slows everyone down.

A Better Workflow

The correct solution is to ensure your feature branch is always up to date with the main branch before you merge it. And you must do this in a way that correctly re-sequences your new migration. The tool for this is git rebase.

Here is a simple step by step process to follow every time you are ready to merge a branch that contains a database migration.

The Scenario

You have a feature branch called feature/user-profile. You have finished your work including a new migration.

While you were working someone else merged a change into the main branch that also included a migration.

The Steps

1. Save Your Work

First commit all your work to your feature branch. This includes your code changes and the migration file you generated.

# On your branch feature/user-profile
git add .
git commit -m “feat: Add user profile page and migration”

2. Get the Latest Changes

Fetch the latest changes from the remote repository so your local copy of main is up to date.

git fetch origin

3. Undo Your Local Migration Commit

This is the most important step. You need to remove the migration you created so you can recreate it in the correct order. We use git reset to undo the last commit but keep the changes in our working directory.

# This assumes the migration was in your last commit
git reset HEAD~1

After running this your migration commit is gone. If you run git status you will see all your changed files including the old migration file as uncommitted changes.

Delete the old migration file. It is now invalid.

# Example for a Rails app
rm db/migrate/20231026100000_add_user_profile.rb

4. Rebase Your Branch

Now apply your changes on top of the latest main branch. Since you have not yet committed your work this rebase will be clean and will simply fast forward your branch to be based on the newest code.

git rebase origin/main

Your branch now includes the new migration from the other developer.

5. Re-generate Your Migration

Now that your branch is up to date you can generate your migration again. The framework will give it a new timestamp that correctly follows the other migration that was just merged into main.

# Example for a Rails app
rails generate migration AddUserProfile

This will create a new file like db/migrate/20231026100600_add_user_profile.rb. The timestamp will be later than the one from the main branch.

6. Commit and Push

Finally commit all your work again. This includes your code changes and the new correctly ordered migration file.

git add .
git commit -m “feat: Add user profile page and migration”

Now your feature branch can be merged into main without any conflicts. You may need to force push your branch if you had pushed it before but this is safe because you are the only one working on it.

This habit might feel like a chore at first. But it’s the kind of discipline that keeps a team moving fast. A clean main branch is a gift to your future self and to every other engineer on the team. Take a moment to think about your own workflow and tell me, how does your team handle database migration conflicts?

— Rishi Banerjee
September 2025