How to Use Feature Flags for Safe Database Migrations
Rolling back database migrations is hard. When code and schema changes are deployed together, a failed migration means untangling both the database changes and the code changes. I learned this the hard way after a production incident where I had to coordinate a database revert with a code rollback, and the whole process took hours instead of minutes.
Feature flags changed how I approach database migrations. Instead of one massive migration that couples schema and code changes, I now break changes into thin slices that can be rolled back by flipping a configuration flag, no database revert required.
The Problem with Traditional Migrations
Traditional database migrations bundle schema changes and code changes together. When you deploy, you run both at the same time. If something goes wrong, you need to revert both. This creates several problems:
- Rollback complexity: Reverting a database migration is not always straightforward, especially if data has been modified.
- Tight coupling: Code expects specific schema, schema expects specific code.
- All-or-nothing risk: Either the entire migration succeeds, or you roll back everything.
The Feature Flag Solution
Feature flags decouple code deployment from schema activation. The idea is simple: deploy schema changes that are backward compatible, then use a flag to control which code path (old or new schema) is active.
Here’s the lifecycle:
+------------------+ +------------------+ +------------------+| 1. Add Schema | -> | 2. Toggle Code | -> | 3. Migrate Data || (backward | | (flag off) | | (gradually) || compatible) | | | | |+------------------+ +------------------+ +------------------+ | v+------------------+ +------------------+ +------------------+| 6. Drop Old | <- | 5. Toggle Old | <- | 4. Enable New || Schema | | Code Off | | Code Path || (next release) | | | | (flag on) |+------------------+ +------------------+ +------------------+If any step fails, flip the flag back. The old code path still works because the old schema is still there.
A Practical Example: Migrating from Password Auth to OAuth
Let’s say I need to migrate user authentication from password-based to OAuth-based. Here’s how I would approach this the traditional way versus the feature flag way.
Traditional Approach (Risky)
# Dangerous: one migration changes everythingclass ChangeUserAuthentication < ActiveRecord::Migration[5.0] def change remove_column :users, :password_hash # Removed immediately! add_column :users, :oauth_provider, :string add_column :users, :oauth_token, :string endendThis migration is risky because:
- The
password_hashcolumn is gone immediately - Any code expecting that column will break
- Rolling back requires adding the column back
- Data migration (converting passwords to OAuth) is not handled
Feature Flag Approach (Safe)
Step 1: Add new columns (backward compatible)
class AddOAuthColumns < ActiveRecord::Migration[5.0] def change add_column :users, :oauth_provider, :string add_column :users, :oauth_token, :string # Keep password_hash column - don't remove yet endendThe old code still works. The new columns exist but are not used.
Step 2: Write code that handles both paths
class UserAuthentication def authenticate(user, credentials) if FeatureFlags.enabled?(:oauth_auth) OAuthService.authenticate(user, credentials) else LegacyPasswordAuth.authenticate(user, credentials) end endendThe flag is off by default. All traffic uses the legacy path.
Step 3: Gradually enable new path
I enable the flag for a small percentage of users, monitor for errors, and gradually increase. If something goes wrong, I flip the flag back to false.
Step 4: Migrate existing data
class MigrateUsersToOAuth def migrate(user) return if user.oauth_provider.present?
# Migrate user to OAuth based on their existing credentials oauth_credentials = OAuthService.convert_from_password(user.password_hash) user.update!( oauth_provider: oauth_credentials[:provider], oauth_token: oauth_credentials[:token] ) endendStep 5: Remove old code path (flag permanently on)
After all users are migrated and the new system is stable, I remove the conditional code:
class UserAuthentication def authenticate(user, credentials) OAuthService.authenticate(user, credentials) endendStep 6: Drop deprecated schema (next major release)
class RemoveLegacyPasswordColumn < ActiveRecord::Migration[6.0] def up remove_column :users, :password_hash end
def down add_column :users, :password_hash, :string endendThis happens in a later release, giving plenty of time to ensure nothing depends on the old column.
Why This Matters for Teams
For small projects, a single migration might be fine. But for larger teams working on shared codebases, feature flags provide critical benefits:
- Independent deployments: Database changes can go out without activating new code behavior.
- Gradual rollout: Enable new features for a subset of users first.
- Instant rollback: Flip a flag instead of reverting a migration.
- Clear ownership: Schema changes and code changes become separate concerns.
As Martin Fowler notes, feature flags are a powerful technique for modifying system behavior without changing code. This applies equally well to database migrations.
Common Mistakes to Avoid
- Performing massive schema changes in one migration: Break changes into thin slices.
- Deploying code and schema simultaneously: Decouple them with feature flags.
- Skipping data migration planning: When changing column types or moving data, plan the migration path.
- Not keeping backward compatibility: The old code path should still work during the transition.
Summary
In this post, I showed how feature flags can make database migrations safer by decoupling code deployment from schema activation. The key is to break large schema changes into thin slices: add new schema elements first, use flags to control which code path is active, migrate data gradually, and clean up deprecated schema in a later release. This approach turns rollback from a database operation into a configuration change, which is much easier to execute and reverse.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments