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: truein 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.
Technical co-founder specialized in SaaS, DevOps, AI agents, and data platforms. Building and scaling with Ruby on Rails, n8n, and fast feedback loops.
UUIDs as Primary Keys in MySQL: A Practical Guide
When to use UUIDs as MySQL primary keys, how B+ trees react, why CHAR(36) hurts, and better options like BINARY(16), UUIDv7, ULIDs, and Snowflake IDs.
Redesign Your Models for Performance in Rails
Most performance issues come from over-normalized models. Learn how scopes, denormalization, and JSON columns simplify queries and remove painful joins.
Rails ORM Performance Best Practices
ActiveRecord makes it easy to ship slow code. Leverage SQL for sorting, filtering, and aggregation - and fix N+1 queries with includes - to keep Rails apps fast.