RL ROLAND LOPEZ
// 1 min read

Rails Migration Best Practices

Rails Migration Best Practices — Three rules that save you from broken deploys: never modify a committed up method, never call models from migrations, and always provide a reversible down.

Never modify the up method on a committed migration

Modifying up methods in committed migrations introduces potential chaos in your deployment and team synchronization processes.

# db/migrate/20230101120000_example_migration.rb
class ExampleMigration < ActiveRecord::Migration[6.0]
  def up
    add_column :users, :email, :string, null: false
  end

  def down
    remove_column :users, :email
  end
end

Never change the up method once committed — you avoid forcing your team into error-prone database version synchronizations.

Never use external code in a migration

Have you ever been onboarded onto a team where a migration would not pass due to some inexplicable error in a model?

Invoking model classes or other Ruby code inside migrations can result in migrations that fail in the future, as external dependencies change or get removed.

In simple words:

  • Day 1: you add Post.create(title: "Migration Best Practices") in your migration.
  • Day 2: another dev adds validates :content, presence: true in the Post model.
  • Day 3: your new dev cannot run the migration done on day 1 because of a validation error.

What do you do? Change a committed migration?

The solution: execute raw SQL. No ORM in migrations.

# db/migrate/20230101121000_add_jobs_count_to_user.rb
class AddJobsCountToUser < ActiveRecord::Migration[6.0]
  def up
    add_column :users, :jobs_count, :integer, default: 0
    # Poor approach: depend on the User model which might change
    # Good approach: use pure SQL for reliability
    execute <<-SQL
      UPDATE users
      SET jobs_count = (SELECT count(*) FROM jobs WHERE jobs.user_id = users.id)
    SQL
  end

  def down
    remove_column :users, :jobs_count
  end
end

Use SQL statements in migrations to avoid coupling to your application’s evolving business logic.

Always provide a down method

Every migration should come with a way to undo its changes, ensuring schema management is as flexible as it is reliable.

If a migration cannot be reversed, explicitly raise an exception.

# db/migrate/20230101121500_create_users.rb
class CreateUsers < ActiveRecord::Migration[6.0]
  def up
    create_table :users do |t|
      t.string :name
      t.timestamps
    end
  end

  def down
    drop_table :users
  end
end
💡

Sometimes you cannot afford to invest time in a down method. That is fine — raise an error and communicate with your team before merging. Once it is committed, it is hard and risky to edit a committed migration.

Roland Lopez
Written by
Roland Lopez

Technical co-founder specialized in SaaS, DevOps, AI agents, and data platforms. Building and scaling with Ruby on Rails, n8n, and fast feedback loops.

Built by Agent Skynet See the agency