LaunchKit

Feature flags in Rails

Six toggles, no gem, and a disabled module that answers 404 instead of rendering half of itself. The registry that lives in code, the overrides that live in a jsonb column, and the one word in the gate that decides whether a stranger learns the route exists.

Every optional module in an application eventually needs an off switch, and the reason is rarely the one people plan for. The AI provider starts returning 500s during a demo. A buyer wants the referral program gone because their product has no referrals. Sign-up has to close for a day while support catches up. None of those should be a deploy.

What a flag has to switch off

A feature in a Rails application is almost never one screen. Take the referral program in this codebase: a controller at /referral, a link in the sidebar, a section in the admin, a cookie read on every sign-up, and a mailer. Switching the feature off means all five behave as if none of it were built.

Most flag code covers the link and stops there, which produces the worst version of the feature: gone for anyone who follows the navigation, present for anyone who typed the URL once and bookmarked it. The question of what the controller itself answers is the whole design, and a disabled feature that answers 404 is where that decision gets argued, along with the one word that keeps the answer honest for visitors who are not signed in.

Where the answer comes from

Two places, and the split is deliberate. The list of flags lives in code, as a frozen array of Data objects, each carrying a key and a default. The admin's overrides live in a jsonb column on the settings row. A flag with no override falls back to its definition's default, and every feature ships defaulting to on.

Putting the registry in code rather than in the database means a flag cannot exist without a developer adding it, which is the correct constraint: a flag is a branch in the program, and branches are not configuration. Putting the overrides in a column rather than in code means flipping one is a PATCH from the admin instead of a deploy. Building Rails feature flags without a gem works through what that costs, including the query it adds to every request and the cast that stops the string "false" from reading as true.

Six flags, and what each one owns

ai, referrals, blog, api, support and signup. Each maps to a module a buyer might not want, and signup is the one that behaves least like the others: turning it off closes public registration, hides every sign-up link, and removes any free plan from the pricing table, while leaving existing accounts and paid checkout untouched.

The registry is short on purpose. Six global booleans is a tool for shipping a product somebody else will run, not an experimentation platform, and the distance between those two things is larger than it looks from the first flag.

What this is not

Not Flipper, not LaunchDarkly, not Unleash. There is no per-user targeting, no percentage rollout, no group membership, no audit trail of who flipped what, and no way to enable a feature for your own account before anyone else's. A flag here is one boolean for the entire installation.

That ceiling is worth naming early, because the migration away from it is not gradual. The moment a flag needs to answer differently for two users in the same request cycle, the storage, the cache and the call signature all change at once, and a codebase with thirty global flags has thirty call sites that assumed no argument.

Proving a flag actually closed the door

A flag is tested by asserting the closed case, which sounds obvious and is where the interesting failure lives. The contract is shared across six request specs here, and one line inside it turns out to do nothing at all. Testing feature flags in Rails takes that line out, runs the suite, and uses what survives to explain when the settings cache goes stale and when it cannot.

Articles on this topic

  • Testing feature flags in Rails

    One shared example, six request specs, and four mutations run against them to find out what they actually pin down. One of the four is caught by nobody, and the line responsible is inside the test.

  • Rails feature flags without a gem

    A frozen registry of Data objects, a jsonb column of overrides, and one method that resolves the two. Why an unknown key is false, why the stored value has to be cast, and the query this adds to every request.

  • A disabled feature should answer 404

    What a controller returns when its feature flag is off, and why the word prepend is the difference between a 404 and a redirect to your sign-in page. Ten lines of concern, one of which is load bearing.