Deploy is not release

"Explain the distinction between deploy and release, and what it buys you."

What it is

Deployment is moving code onto infrastructure. Release is exposing behaviour to users. They are separate events, and a feature flag is what separates them.

Under the coupled model, git push eventually means "users see it", so the deploy is the release, and every deploy carries the full risk of the change. Under the decoupled model, code ships dark (deployed, not exposed) and is turned on later, independently, for a chosen population, by someone who may not be an engineer.

Commonly confused with canary deployment. A canary is a deploy technique: shift traffic gradually to a new version. A flag is a release technique: expose a behaviour to a chosen population. They compose, and confusing them is why teams think they have decoupled when they have not.

The problem it solves

Coupling deploy and release creates a chain of consequences that most teams experience without diagnosing:

Deploys become dangerous, so they become rare. If deploying means releasing, each one carries the risk of every change in it. Risk-aversion produces batching, batching produces bigger deploys, bigger deploys are riskier, and the loop tightens.

Rollback is slow and coarse. Reverting a behaviour means redeploying the previous artifact, which is minutes at best, and it reverts everything in that artifact including unrelated fixes.

Long-lived branches become necessary. If merging to trunk means shipping, an unfinished feature cannot be merged, so it lives on a branch for weeks and integrates painfully. This is the mechanism behind most merge-hell.

Release timing is an engineering decision. Marketing wants the feature live at 9am Tuesday for the launch; that becomes a deploy scheduled at 9am Tuesday, which is the worst possible time to change production.

One sentence resolves all of it: deploy continuously, release deliberately.

Mechanics

The shape

// Deployed on Monday. Released on Thursday, to 5 percent, by a PM.
if (flags.isEnabled("checkout-v2", user)) {
    return checkoutV2.process(cart);
}
return checkoutV1.process(cart);

That branch is the entire mechanism, and everything below is consequence.

The four flag types, which have different lifecycles

Treating all flags the same is the most common operational mistake, because a release flag and a kill switch have opposite expectations.

TypeLifetimeOwnerRemoval
ReleaseDays to weeksEngineeringDeleted after rollout. Non-negotiable
Ops / kill switchPermanentEngineering / SRENever; it is infrastructure
ExperimentDuration of the testData / productDeleted when the experiment concludes
Permission / entitlementPermanentProductNever; it is a business rule

Only the first two are the concern of a deployment discussion. Confusing an experiment flag with a release flag is how you end up with a permanent A/B test nobody is reading.

Flag debt, and the policy that prevents it

Stale flags are dead code paths with untested interactions. Twenty stale boolean flags is $2^{20}$ nominal configurations, and while almost all are unreachable, you cannot easily prove which. The result is a codebase where nobody can predict what a given user experiences.

The policy that works, and it must be mechanical rather than cultural:

# Every release flag declares an owner and an expiry at creation.
- key: checkout-v2
  type: release
  owner: payments-team
  created: 2026-08-03
  expires: 2026-09-15        # CI warns at 80%, fails the build after

Then CI enforces it: warn as expiry approaches, fail the build after. Plus a recurring report of flags at 100 percent for more than 30 days, which is the signature of a rollout that finished and was never cleaned up. Flag hygiene is the tell of an experienced operator, because everyone knows to add flags and few teams have a policy for removing them.

What decoupling actually buys, concretely

Rollback becomes seconds, not minutes. Flag off propagates in the time it takes your flag system to push, typically single-digit seconds. A redeploy is minutes, and during those minutes users are still hitting the bad path.

Trunk-based development becomes possible. Unfinished work merges to main behind a disabled flag, so branches live hours rather than weeks and integration is continuous. This is the change with the largest second-order effect, because it removes merge hell as a category.

Release becomes a business decision. A PM enables the feature for a customer segment at a chosen time without an engineer, so the launch calendar stops driving the deploy calendar.

Progressive exposure is independent of deploy. 1 percent, 5, 25, 100, with automatic rollback on a metric regression, all without touching infrastructure.

Deploy frequency goes up because deploys got boring. A deploy that changes no user-visible behaviour is a low-stakes event, and low-stakes events happen often, which shrinks the change size, which reduces risk further.

Where it composes with everything else

Merge to trunk behind a disabled flag
  -> CI: tests, contract checks, security scan, immutable signed artifact
  -> Deploy to production (canary + automated analysis on the VERSION)
  -> Bake, promote to 100 percent of infrastructure
  -> [days later] Enable the FLAG at 1 percent, ramp on business metrics
  -> Delete the flag, delete the old code path

Two independent progressive rollouts: the canary derisks the deploy (is the version safe), the flag ramp derisks the release (is the behaviour good). They answer different questions and use different metrics, which is the same distinction as canary versus A/B testing.

The parts that do not decouple cleanly

Be honest about these, because they are where the pattern leaks:

Database schema. A flag cannot toggle a schema. Schema changes ride the expand-contract pattern: add the new column (both code paths work), migrate, switch reads behind the flag, then contract only after every old code path is gone. The contract phase is gated on the flag's removal, not on the deploy.

Cached and serialised data. If the new path writes a new format and you flag off, the old path reads data it cannot parse. Version your cache keys with the schema, so the old path simply misses rather than crashes. This is the specific failure that turns a flag-off from a rollback into an incident.

Published events. If the new path emits a new event shape, consumers must handle both before you enable it. Consumers deploy before producers, always.

Third-party side effects. A flag-off does not un-send an email or un-charge a card. Anything with an external side effect needs idempotency and compensation, not just a flag.

A worked example

A payments team replacing their checkout flow. Old model: a two-week release train, a four-hour release window on a Thursday evening, and a rollback procedure that has been used twice and worked once.

After decoupling:

Week 1-3   checkout-v2 merged to trunk incrementally behind a disabled flag.
           14 deploys to production. Zero user-visible change. Nobody is
           nervous about any of them, which is the point.

Week 3     Flag enabled for the team's own accounts. Real production, real
           money, six people. Two bugs found that no staging environment
           would have shown, because they involved real card processors.

Week 4     1 percent of traffic. Watched: authorisation rate, checkout
           completion, p99, error rate. Authorisation rate down 0.3 percent.
           Flag off in 4 seconds. Root cause: a 3-D Secure edge case.

Week 4     Fixed, deployed (a normal boring deploy), re-enabled at 1 percent.
           Clean. 5 percent, 25 percent, 50 percent, 100 percent over 8 days,
           gated on the same metrics.

Week 6     Flag deleted. Old code path deleted. 400 lines removed.

The number that makes the case to a director: rollback went from a 25-minute redeploy to a 4-second flag flip, and the bad configuration was live for under a minute instead of half an hour. That is the difference between an incident and an observation, and it is measurable.

The failure this design still had, and it is worth telling: in week 4 the flag-off worked for the code path and not for a cache. The new path had written entries in a new format, and the old path deserialised them and threw. The fix was to version the cache key with the schema version, so the old path missed cleanly rather than reading data it could not parse. That is the concrete instance of "cached data does not decouple", and it is the most common way a flag-off fails.

Production evidence

Continuous delivery as formulated by Humble and Farley makes this distinction central: the deployment pipeline exists to make deploying a business-as-usual event, and separating deployment from release is how you get there.

DORA's research programme consistently finds that deployment frequency and lead time correlate with organisational performance while change failure rate does not have to rise, and the practices that make that possible (trunk-based development, small batches, feature flags) are exactly what decoupling enables. Trunk-based development in particular is not viable without flags, because you cannot merge unfinished work otherwise.

Facebook's "dark launch" of Messenger and Flickr's early feature-flag practice are the canonical origin stories, both describing shipping code to production long before exposing it, precisely to separate the technical risk of deployment from the product risk of release.

LaunchDarkly, Unleash, Flagsmith and every cloud provider's equivalent exist as a product category because managing this at scale (targeting, audit, expiry, kill switches) outgrew configuration files. Their existence is itself evidence of how standard the practice is.

The debate

The credible case against: flags are complexity. Every flag is a branch in the code and a possible state of the system, and the testing burden is real, because you must test both sides of a live flag and the combinatorics grow. A codebase with hundreds of flags is genuinely harder to reason about than one with none, and teams that adopt flags without a removal policy end up worse off than where they started.

The other case against: for a small team deploying a few times a week with fast rollback and low blast radius, the ceremony may exceed the benefit. Ship it, watch it, revert if needed.

My position: decouple, with a hard policy on release-flag removal enforced in CI, because the failure mode of flags is not adding them, it is never deleting them. Ops flags and kill switches are permanent infrastructure and should be treated as such. And the biggest single benefit is not rollback speed, it is that trunk-based development becomes possible, which removes long-lived branches as a category.

Flags are the wrong tool for schema changes (use expand-contract), for anything with irreversible external side effects (use idempotency and compensation), and as a substitute for testing, which is the failure mode where teams ship less-tested code because "we can always flag it off". You cannot flag off a charged card.

Follow-up Q&A

"Deploy versus release: explain the distinction and what it buys you." Deployment moves bits onto infrastructure; release exposes behaviour to users; a feature flag decouples them. What it buys: rollback in seconds rather than a minutes- long redeploy, and only the offending behaviour rather than everything in the artifact. Trunk-based development, because unfinished work can merge behind a disabled flag, which removes long-lived branches. Release timing becomes a business decision rather than a deploy schedule. And deploys become frequent because they became boring, which shrinks change size and reduces risk further.

"What is flag debt and how do you prevent it?" Stale flags are dead code paths with untested interactions, and twenty booleans is nominally a million configurations you cannot reason about. Prevention has to be mechanical: every release flag declares an owner and an expiry at creation, CI warns as expiry approaches and fails the build after, and a recurring report lists flags at 100 percent for more than 30 days, which is the signature of a finished rollout nobody cleaned up. Ops flags and permission flags are exempt because they are permanent by design, which is why typing flags matters.

"What does not decouple?" Four things. Database schema, which needs expand-contract with the contract phase gated on flag removal rather than on deploy. Cached and serialised data, because flagging off means the old path may read data written in a new format, so version cache keys with the schema. Published events, because consumers must handle the new shape before producers emit it. And anything with an irreversible external side effect, because a flag-off does not un-send an email or un-charge a card.

"How is this different from a canary?" A canary shifts traffic to a new version and asks whether it is safe, using operational metrics over minutes to hours, owned by the deploy system. A flag exposes a behaviour to a chosen population and asks whether it is good, using business metrics over days, owned by product. They compose: a change is deployed behind a canary, promoted to 100 percent of infrastructure, and only then does the flag ramp begin.

"A flag-off didn't fix the incident. What went wrong?" Almost certainly state. The new path wrote something the old path cannot read: a cache entry in a new format, a database row with a new field the old code requires to be absent, an event consumers now expect. Flags decouple code paths, not data. The fix is to make the old path tolerant, usually by versioning cache keys and by keeping the schema backward-compatible until the flag is deleted. It is worth testing the flag-off path explicitly, because everyone tests turning a flag on and almost nobody tests turning it back off with data already written.

Common misconceptions

The most common is that flags are for A/B testing. Experiments are one of four flag types, with a different owner and a different lifecycle, and conflating release flags with experiment flags produces permanent experiments nobody reads.

The second is that a flag is a rollback. It reverts a code path, not the data that path wrote, which is why flag-off failures are almost always state failures.

The third is that flags reduce the need for testing. They reduce the blast radius of a defect. Shipping less-tested code because it is behind a flag is how a team ends up with a fast rollback from a problem they created.

Interview delivery note

Lead with the sentence, because it does most of the work: "Deployment moves bits, release exposes users, and a feature flag decouples them. Deploy continuously, release deliberately."

Then the consequence that matters most, which is not the obvious one: "The benefit people name first is rollback in seconds instead of a redeploy, and that's real. But the bigger one is that trunk-based development becomes possible, because unfinished work can merge behind a disabled flag. That removes long-lived branches and merge hell as a category."

The depth signal is flag hygiene and the leak: "the failure mode of flags isn't adding them, it's never deleting them, so every release flag gets an owner and an expiry enforced in CI. And I'd be explicit that flags decouple code paths, not data: if the new path wrote a cache entry in a new format, flagging off hands the old path something it can't parse. Version cache keys with the schema."

Further reading

  • Humble and Farley, Continuous Delivery, on the deployment pipeline and separating deployment from release.
  • Martin Fowler's bliki entry on feature toggles (Pete Hodgson), for the four toggle categories and their differing lifecycles.
  • The DORA State of DevOps reports, for the relationship between trunk-based development, small batches and delivery performance.
  • Documentation from any managed flag platform on flag lifecycle, targeting rules and archival policy, for how the hygiene problem is handled at scale.