Using Feature Flags for Safe Ruby on Rails Version Upgrades
In civil engineering, replacing a load-bearing support while traffic continues to flow overhead requires meticulous structural planning. Engineers do not tear down the bridge and halt commerce; instead, they build parallel supports, gradually shift the weight, and remove the old structures only when the new ones carry the load securely.
Software modernization requires the exact same level of care. When we need to upgrade a Ruby on Rails application, we frequently face a conflict between product momentum and technical maintenance. For large and complex applications, a “big bang” release — where the entire framework and its dependencies are updated in a single, massive deployment — introduces substantial risk. This approach, of course, often leads to extended downtime, unpredictable regressions, and a halt to feature development.
Technical debt remediation requires a more pragmatic strategy. We need a way to deploy the upgraded codebase safely, monitor its performance, and roll back instantly if something goes wrong. This is where feature flags become a critical component of a reliable migration to a new framework version.
What Are Feature Flags in an Upgrade Context?
Before we get into implementation details, though, let’s take a step back and define our terms. Feature flags, strictly speaking, are conditional statements embedded in your application code that allow you to enable or disable functionality dynamically. While development teams typically use them for rolling out new product features to specific user segments, they are equally valuable for structural architectural changes.
During a Ruby on Rails upgrade, feature flags allow you to run both the old and new execution paths within the same production environment. We can route a small percentage of traffic through the upgraded logic, monitor the system’s technical health, and gradually increase the load. If errors occur, you disable the flag to restore the previous state immediately, achieving a safer, zero-downtime deployment.
Implementing Feature Flags in Rails
To illustrate this, let’s look at a concrete example. Suppose we are upgrading our application’s PDF generation from an older, unsupported library (like wicked_pdf) to a modern headless browser approach (like grover), as the older library is incompatible with our new Rails version. We need to ensure the new syntax and payload structure works correctly in production.
We could implement a basic feature flag using a standard environment variable check:
class ReportGeneratorJob
def perform(user_id)
user = User.find(user_id)
if ENV['USE_NEW_PDF_ENGINE'] == 'true'
# New implementation for upgraded framework
GroverPdfGenerator.new(user).execute
else
# Legacy implementation
WickedPdfGenerator.new(user).execute
end
end
end
This approach, though, has significant limitations. Using environment variables means we have to restart the application server whenever we want to toggle the flag, defeating the purpose of a rapid, zero-downtime rollback.
Instead, we typically rely on a dedicated library. There are a number of different feature flag libraries which are used in production; rollout and split are both widely used. However, for our example here, we’ll use the flipper gem. It allows us to manage these toggles dynamically, often backed by Redis or our primary database.
First, let’s add the necessary gems to our Gemfile:
gem 'flipper'
gem 'flipper-redis'
Next, we can install them:
$ bundle install
With Flipper, our implementation becomes much more resilient:
class ReportGeneratorJob
def perform(user_id)
user = User.find(user_id)
if Flipper.enabled?(:modern_pdf_generation, user)
GroverPdfGenerator.new(user).execute
else
WickedPdfGenerator.new(user).execute
end
end
end
Because Flipper provides a Ruby API, we can toggle this feature flag directly from the Rails console. For example, if we wanted to enable it globally, we could run:
$ bin/rails console
irb(main):001:0> Flipper.enable(:modern_pdf_generation)
=> true
You also may notice that we passed the user object to Flipper.enabled?. This is significant because it means we can selectively enable the feature for internal testing accounts before exposing it to our entire customer base.
Designing a Battle-Tested Workflow
Implementing feature flags for a framework upgrade requires careful planning. We cannot realistically wrap every line of code in a conditional block. Instead, we must isolate major integration points, third-party API calls, and complex database queries.
Gradual Version Bumps
A successful strategy relies on gradual version bumps rather than attempting to leapfrog several major releases at once. When we upgrade incrementally, the surface area for bugs decreases. Feature flags complement this by letting you test specific deprecation fixes in production before fully committing to the new version.
For example, if an upgrade requires replacing an outdated gem, we can use a feature flag to route a subset of background jobs to the new dependency. This allows you to verify the dependency management strategy without exposing the entire system to potential failure.
Mitigating Security Risks
Major framework upgrades often address critical security risks by patching vulnerable dependencies. The upgrade process itself, however, can introduce new vulnerabilities if we are not careful. By using feature flags, security teams can audit the new code paths in the production environment with restricted access, ensuring that the upgraded application maintains or improves its security posture before general availability.
The Trade-Off: Feature Flag Technical Debt
One may wonder: if feature flags are so useful, why not keep them around permanently as safety nets? The answer is straightforward: feature flags introduce a different kind of technical debt in the form of code complexity.
Every feature flag creates a branch in your code’s execution path. If you have three active flags interacting in the same controller, you now have eight possible execution paths to test and maintain. Over time, abandoned flags clutter the codebase and make reasoning about the system significantly harder.
To mitigate this, feature flags used for framework upgrades must be strictly temporary. Once the new code path has been verified in production and running successfully for an appropriate monitoring period, you must follow up with a pull request to remove the flag and the legacy code. The upgrade is not truly complete until the old bridge is entirely dismantled.
The Economics of Post-Upgrade Support
The work does not end when the new framework version is deployed. Post-upgrade support is a critical phase where latent bugs often emerge. Feature flags act as a persistent safety net during this period. If a performance bottleneck appears under peak load, engineering teams can toggle the problematic code path back to the legacy implementation while they investigate.
This approach significantly alters the economics of a modernization project. Instead of requiring an entire team to be on standby for a high-risk deployment weekend, the rollout happens smoothly during regular business hours. For organizations navigating this complexity, having these safety mechanisms correctly implemented is essential for maintaining business continuity.
The Long-Term Perspective
Feature flags provide the control mechanism necessary to manage the inherent risks of framework modernization. By decoupling deployment from release, they enable engineering teams to execute a Ruby on Rails upgrade safely, maintain continuous delivery, and ensure long-term stability for their applications. Rather than treating an upgrade as a massive, disruptive event, we can integrate it into our standard development lifecycle.
Sponsored by Durable Programming
Need help maintaining or upgrading your Ruby on Rails application? Durable Programming specializes in keeping Rails apps secure, performant, and up-to-date.
Hire Durable Programming