A feature flag looks like the least architectural thing in the world. It’s an if statement with
a dashboard bolted on — if (flags.newCheckout) { … } — the sort of thing you’d add in thirty
seconds and never mention in a design review. Delivery tooling, not architecture. Something the
DevOps people care about.
I want to argue the opposite: that reaching for a flag is one of the more consequential architectural decisions you make, precisely because it doesn’t feel like one. What a flag actually does is split two things that used to be welded together — and once you’ve split them, the shape of your decisions changes.
Deploy is not release
The two things are deploy and release.
Deploying means the code is on the server. Releasing means a user can see it. For most of the history of shipping software these happened at the same instant: the deploy was the release, which is why deploys were tense, why they happened at 2am, why “big bang” is a phrase we say with a wince. The moment the code went out, its behaviour went out with it, to everyone, at once.
A flag prises those two apart. The code deploys dark — present, exercised, but switched off — and you release it later, separately, by flipping a toggle. Deploy on Tuesday to nobody; release on Thursday to 5% of users; release to everyone the following week. The deploy stopped being the event. That’s not an operational convenience. It’s a change to what a deploy means, and that’s an architectural statement about your system.
It turns a one-way door into a two-way door
I’ve written before about one-way and two-way doors — the idea that how hard a decision is to reverse should govern how much you agonise over it. A deploy without flags is close to a one-way door: to undo it you roll back, redeploy, and hope nothing else moved in the meantime. Under pressure, mid-incident, that’s a genuinely frightening door to walk through.
A flag makes the same change a two-way door. “On” becomes a runtime decision you can reverse in seconds, without a build, without a deploy, without waking anyone. That single property changes the economics of the whole exercise. You can ship the risky thing behind a flag and decide in production, with real traffic whether it stays on — which is really just the last responsible moment applied to release. You defer the irreversible commitment until you actually have the information to make it, instead of guessing at build time and living with the guess.
It’s a cousin of deployment coupling, though not the same thing: there, separate components were forced to ship together because they shared a contract; here, a single change is split so that its deploy and its release come apart in time. Different couplings — but both are about keeping control of what actually goes live, and when.
Not all flags are the same thing
Here’s where it goes wrong, though, and where “just add a flag” stops being free. People treat “feature flag” as one concept when it’s really four, with wildly different lifespans and owners. Conflating them is the root of most flag messes I’ve seen.
- Release toggles are temporary. They hide half-built work and drive a progressive rollout, and they are meant to die the moment the feature is fully on. A release toggle that’s still in the code three months after 100% rollout is a bug, not a feature.
- Ops toggles (kill switches) are operational. They let you shed load or shut off a fragile downstream dependency when things are on fire. These can legitimately live for years — but they’re owned by whoever runs the system, not whoever wrote the feature.
- Experiment flags live exactly as long as the experiment. When the A/B test concludes, the flag and the losing branch both go.
- Permission / entitlement flags decide who gets what — which plan tier sees which capability, which cohort is in the beta. These are long-lived by design, but calling them “feature flags” flatters them; they’re business rules, and they belong in something that looks like configuration, not a toggle you might casually flip.
The category error — managing a permission rule like a throwaway release toggle, or leaving a release toggle in place as though it were a kill switch — is how you end up unable to say what any given flag is actually for. And a flag nobody can explain is the dangerous kind.
Every flag is also a liability
Because the cost of a flag isn’t the if statement. It’s that every flag doubles the number of
code paths, and they multiply: n independent flags describe up to 2ⁿ possible states, of which you
meaningfully test approximately none. Your test suite runs one combination — usually “everything
on”. Production runs the others.
The cautionary tale here is Knight Capital. On 1 August 2012 the firm rolled new code to its order-routing system but missed one of eight servers. The new code had reused an old flag — one that years earlier had switched on a piece of long-defunct test logic called Power Peg. On the seven updated servers the flag meant the new thing; on the eighth, still running the old code, it woke Power Peg back up. In roughly forty-five minutes the system fired millions of unintended orders and the firm lost about $440 million — very nearly its entire capital. It did not recover.
You can read that as a deployment failure, and it was one. But the deeper fault was a stale flag: a toggle left in the code long after its purpose had gone, still wired to logic nobody thought was reachable. A flag you didn’t remove isn’t a dormant control. It’s dead code wearing the costume of a control — and one day someone flips it.
The discipline: a flag is a decision with a lifecycle
So the architectural move isn’t “use feature flags.” It’s treating each flag as a decision that has a beginning and an end. In practice that means a few unglamorous habits:
- Every release toggle gets an owner and a removal trigger the day it’s created — “this comes out when checkout is at 100% and has been for a week” — not a vague intention to tidy up later.
- Removing the flag is part of finishing the feature, not optional cleanup that competes with the next thing on the board. The feature isn’t done until the branch it replaced is gone.
- The genuinely long-lived flags — kill switches, entitlements — are named as such and owned accordingly, so nobody mistakes them for litter and nobody mistakes the litter for them.
If that sounds like the reasoning you’d capture in an architecture decision record, that’s because it is one. “We put this behind a flag because the blast radius justified a gradual rollout, and it comes out on this trigger” is exactly the kind of decision-and-its-expiry that the next person needs to inherit.
What you’re optimising for
As ever, it depends on what you’re buying and what you’re willing to pay. Flags buy you reversibility, a split between deploy and release, progressive rollout, and safe experimentation. They cost you complexity, an exploding test surface, and a standing obligation to clean up after yourself. That’s a trade worth making when the reversibility genuinely matters — a risky change, a wide blast radius, a migration you want to creep out one percent at a time. It’s a poor trade when you’re wrapping a change you’ll never turn off in a code path you’ll never remove, buying optionality you have no intention of using.
The flag itself was never the interesting part. The interesting part is that you’ve moved a decision from deploy time to run time, and from irreversible to reversible — and, like any architectural decision, it’s one to make deliberately and then follow through on, rather than sprinkle around and walk away from.