We used Flipper’s feature flags to iteratively build out everything we needed to offer a free Cloud account to anyone. And since we had been updating, releasing, and testing various elements to support free plans over the course of the last several months, we had a painless release of a massive change on a Friday afternoon.
That’s the magic of feature flags. They can introduce some new workflow challenges and considerations, but if managed carefully, the net peace of mind is priceless. It's somewhat tongue-in-cheek that we named the blog "The Friday Deploy," but it's also a nod to our vision.
Ultimately, feature flags are about letting teams separate the process of releasing new features from the process of shipping code. When shipping code, frequent small changes reduce risk dramatically, but for features, we often need a large amount of code to change all at once. With these two elements at odds, de-coupling one from the other enables better workflows at every step of the process.
We enabled our free plan on a Friday afternoon with no surprises, so we wanted to share some of the behind-the-scenes details on how we made such a large change safely and reliably. Mostly it boils down to the fact that we had been deploying elements of the free plan functionality over the course of about 20 separate pull requests over multiple months. So we simply never had a single large event with everything riding on it.
Background & Context
In a way, Flipper has always offered a free option by virtue of being open-core. Anyone could use the
flipper gem locally and optionally add
flipper-ui. But that approach can only ever do so much, and it left a lot of the burden on teams to build more robust feature flipping functionality.
Flipper Cloud, on the other hand, offers features that come in handy as soon as a team grows beyond a couple of people. So while the open source Flipper gem can help you store and check basic feature flag data, Flipper Cloud adds an additional layer of benefits that help streamline development and managing feature flag states across multiple projects and environments from a single location.
Offering a free option within Flipper Cloud would require re-working a fair amount of logic. It's a big change, and releasing widespread large changes can be chaotic and stressful. Using a feature flag to suppress the new free plan work let us work iteratively and regularly release incremental updates to production without any impact to customers until we were ready to turn it on.
At each of the following steps, we had a
- Pull Request.
- automated tests.
- code review.
- release to staging.
- manual test in staging with the flag on and off.
- promotion to production.
This kept the code changes small and focused making them much easier to review and release almost daily. As a result, we were able to maintain steady momentum even though no one on the team was working on it full time.
All-in, we had a little over 20 pull requests and releases just related to free plans over about 90 calendar days or about 30 working days. During that time, we were also able to keep working on some other smaller items, quickly handle dependency updates, and generally just keep moving forward.
Step 1: Find the Pivot Point and Add the Flag
Prior to the free plan, customers either had an active subscription, or they didn’t. And by extension, they either had access to all of the feature or none of them. In order to add a free option, we needed a way to manage which accounts received which features. That meant adding extensive new logic around features/entitlements.
Prior to our free option, it was mostly a binary question of whether they had an active subscription or not. But with free plans enabled, the capabilities would be determined by the specific plan rather than by the presence or absense of an active subscription.
So that became the primary pivot point in determining how to handle the free plan flag. And since those checks would almost always go through the current organization, we could define the flag there for easy access and less repetition.
class Organization < ApplicationRecord ... def free_plan_enabled? Flipper.enabled?(:free_plan, self) end # flag:free_plan - Remove :active_subscription? after enabled def active_subscription? # current_subscription&.active? free_plan_enabled? || current_subscription&.active? end ... end
That helped mitigate the proliferation of
Flipper.enabled?(:free_plan, organization) checks that would otherwise be sprinkled throughout the codebase. Instead, anywhere we have an organization in scope (which is most everywhere), we could easily check the flag for that specific organization. Then we could swap the logic for entitlements checks between “do they have an active subscription?” and “what plan are they on?”
Step 2: Build the New Entitlements Logic Behind the Flag
Prior to the free plan, customers either had an active subscription, or they didn’t. After adding the plan, any customers that didn’t have an active subscription would implicitly be on the free plan without needing a subscription of any kind. So afterwards, the presence or absence of a subscription would no longer be the deciding factor.
While the free plan was disabled, Flipper kept on using the presence of an active subscription to determine whether a given organization should have access. After enabling the free plan, Flipper needed to look at the organization’s entitlements based on its plan—either the subscribed plan or falling back to the free plan. That led to the creation of an
Entitlements class where all of the capabilities could be defined based purely on the organization’s plan.
By encapsulating the entitlements, it was easier to write tests that ensured each plan translated to the correct capabilities. And since the entitlements only came into play once the free plan was enabled, we could be confident that they only needed to be tested in the context of having the free plan enabled.
That meant that we could keep the tests manageable in the contexts where we needed to ensure that we tested both the enabled and disabled path. As a result, we could handle most of the duality at the organization level. In most cases, that boiled down to simple capability checks that could handle the flag directly within the organization model.
def class Organization < ApplicationRecord ... def feature? free_plan_enabled? ? entitlements.feature? : active_subscription? end ... end
With the majority of the flag checks contained to the organization, we could be confident that entitlement checks wouldn’t adversely affect anyone as long as the free plan was disabled. Because as long as the free plan was disabled, nothing even looked at the new entitlements class.
Step 3a: Apply Entitlements in Access Policies
We use policy objects with Pundit for authorization. In order to enforce any restrictions from the entitlements, we needed to update the relevant policies to check the organization’s entitlements in addition to the existing permission checks.
However, we needed to make sure those policies didn’t incorrectly trigger limitations prior to flipping the switch on fire plans. With the entitlements for the organization handling most of the flag-based logic, we could trust that
organization.<feature>? would have the correct behavior both before and after we turned on free plans. That is, the new entitlement policies wouldn’t even be referenced as long as the free plan was disabled.
So we were able to add the new entitlements checks and confidently deploy them all to production without fear of accidentally restricting access to customers who shouldn’t have restrictions.
Step 3b: Update Interface Elements & Copywriting
In addition to the internal application logic for entitlements, adding a free plan meant a fair amount of copywriting and informational updates within the application. While our paid plan still includes a free trial, there’s no need for a trial period with the free plan. So in addition to the organization flag, we had a fair amount of situations where we needed some of the copy and content to adapt for free plans once they were live.
Fortunately, these were all cases where a call to
organization.free_plan_enabled? let us display different content dynamically. This created a little bit of extra clean up after we launched, but it was incredibly easy to search for and remove. In most cases, we could remove it, run the tests, let the relevant test fail, and then remove the legacy test too.
This process of implementing entitlements, policies, and the necessary interface changes worked great, and feature by feature, we implemented each plan-based restriction end-to-end, deployed it to production, and started on the next restriction separately.
Flipping the Switch on a Friday
Once everything was implemented, we turned on free plans in the middle of a Friday, but we noticed a couple of minor issues that still needed some love. So we just turned it off again and gave ourselves some time to smooth out the rough edges.
About an hour later, we had implemented the updates and released them to production. We flipped the free plan on again and let it ride. Given the scope of changes, we held off on any announcements and gave it the weekend to see if any bugs popped up. Fortunately, everything went smoothly after that, and we were able to make a significant release with widespread changes through all areas of the application without any major surprises.
Once we felt good about the overall state of the application with free plans enabled for a few days, we updated our test suite to run all tests with the free plan fully enabled. It uncovered a handful of spots where we needed to update tests, but they were all minor fixes primarily for the test fixtures. In hindsight, I would have preferred to be regularly running the full test suite with the feature fully enabled much earlier in the process, but we had good coverage as it was.
Once all of the tests were updated and passing with free plans fully-enabled, we were able to start removing the various calls to enable or disable the feature where we had written tests for both the enabled and disabled paths. That involved deleting the tests in the disabled state and removing the now-unnecessary
Flipper.disable(:free_plan) calls in the remaining tests.
Elsewhere, it was just a search for
:free_plan to find and remove all of the cases where we fell back to the legacy logic. Again, thanks to the tests, these were easy to find and easy to verify.
Using feature flags don't automatically make big releases painless, but they do enable more iterative workflows that can help catch problems earlier and rely on a higher quantity of smaller releases that make code reviews easier and reduce the risk of big releases creating big problems. They add a little bit of overhead, but the workflows enabled by having a predictable approach to feature flags makes it absolutely worth it. Like they say, an ounce of prevention is worth a pound of cure.
Ultimately, this boils down to separating shipping features from shipping code. That means no long-running branches with tedious amounts of merge conflicts. Pull request are smaller, more focused, and easier to review. Frequent smaller releases reduce the risks that come with larger sweeping releases. Being able to enable or disable a feature without re-deploying makes launching features more convenient. And when you need to rollback? It's instant instead of waiting on a deploy.
As a software developer, it makes everything run more smoothly, and with our new free plan, you don't even need to take our word for it. You can take it for a spin with no risk.