Skip to main content

Overview

A release is the set of decisions about when and how a change becomes visible to users, independent of when its code reaches the production environment. The core principle is separating deploy from release: deploying is the mechanical act of shipping code, releasing is the decision to expose a feature. Without that separation, releases are tense events. Changes pile up into a large batch waiting for the announcement date, unverified features reach users the moment their code merges, and recovery from a problem depends on another deploy. With the separation in place, deploying becomes a routine step repeated many times a day, and releasing becomes a low-risk decision that a single configuration change can advance or reverse. This page explains the principle of that separation, the environment-variable feature flag that implements it with no extra infrastructure, the development flows that deliver features spanning multiple pull requests safely, and how to roll out, roll back, and clean up flags. The pipeline that automates the deploys themselves is covered on the CI/CD page.

Principles

Treat deploy and release as different events

Deploying code and releasing a feature usually happen at the same moment, but they answer different questions and belong to different owners.

Deploy

Reflecting code in the production environment. A mechanical, repeatable step that can be automated and performed many times a day.

Release

Making a feature available to users. A judgment call about timing — coordination with announcements, support readiness, or business milestones.
Keeping the two coupled forces a trade-off: either deploys wait for the business timing, so changes pile up into large risky batches, or features go live the instant their code lands, whether or not anyone is ready.

Deploy small and often, choose release timing deliberately

Separating the two is the foundation of continuous delivery — keeping the software releasable at all times, deploying small changes frequently, and choosing release timing deliberately. Small, frequent deploys improve speed and stability together, because each change is easier to review, test, and undo. The feature flag, covered next, is what implements this separation with no extra infrastructure.

Implementing the separation with feature flags

Start with a single environment-variable branch point

A feature flag is a conditional that switches behavior on or off without a code change. The simplest reliable implementation is an environment variable read at one branch point in the code. In the following example, a new label is rendered only while the FEATURE_NEW_LABEL environment variable is 'true'. Enable it locally to work on the feature, and merge with it disabled in the production environment.
Enabling the feature in the production environment is a configuration change — add FEATURE_NEW_LABEL=true to the environment — and disabling it is removing that line.
Give every flag a shared prefix such as FEATURE_. A search through the environment configuration then inventories the live flags, which also surfaces the forgotten ones covered in the cleanup section below.

Know what the flag unlocks

The flag turns “the code is in the production environment” and “the feature is visible” into independent facts. That unlocks:

Merge unfinished work

In-progress features merge into the main branch behind a flag, invisible to users, instead of aging in a long-lived branch.

Deploy often, release deliberately

Deploy to the production environment many times a day while choosing when to release each feature.

Instant rollback

If a released feature misbehaves, turn the flag off. Recovery time is minimal.

Test safely in the production environment

Enable a feature in the verification environment or for internal use first, while users in the production environment still see the old behavior.
Environment-variable flags can be implemented with no extra infrastructure, which makes them a good first step. Adopting a flag management library or service is also worth considering.

Delivering features that span multiple pull requests

Features too large for a single pull request benefit most from separating deploy and release. The development flow differs depending on whether the change can hide behind a flag, so this section covers two flows together with when to apply each.

Trunk-based development

Trunk-based development is the practice of integrating all work into the main branch in small increments, avoiding branches that live for days or weeks. Long-lived branches accumulate merge conflicts, hide integration problems until the end, and produce large pull requests that are hard to review. Feature flags are what make this practical for features that take more than one pull request to build: each increment merges as soon as it passes review, guarded by the flag, and the feature is enabled only when the last piece lands. The result is that several people can build in the same area with conflicts less likely, and every merge is small enough to review properly. In the following diagram, one feature is split into three increments, each merged into the base branch with the flag off, and the release happens by enabling the flag after the last increment lands. This is the same principle as splitting work into small units — see Task Breakdown for slicing features down to a size that merges behind a flag, and Pull Requests for keeping each merge reviewable.

The topic branch flow

Some changes cannot hide behind a flag — a visual overhaul or a framework migration, where the impact on the production environment cannot be hidden. The topic branch flow keeps granularity in that case. It is a three-layer branch structure that places a topic branch between the base branch and the working branches. Review and integration complete on the topic branch, and the base branch receives nothing until release, so small pull requests keep flowing without affecting the production environment. The flow works as follows.
  1. Cut a topic branch from the base branch, then cut working develop branches from the topic branch.
  2. Review on PRs from a develop branch into the topic branch. This is the stage where granularity is maintained.
  3. When the work is ready for the production environment, merge a PR from the topic branch into the base branch. The review at this point can be minimal, because the changes were already reviewed in step 2.
  4. Merge the base branch’s changes into the topic branch at least once a day to keep conflicts to a minimum.
The trade-off is that the topic branch itself is a long-lived branch: integration with the base branch is deferred, which is exactly what trunk-based development avoids, so the daily sync in step 4 is what keeps conflicts manageable. Prefer feature flags when the change can hide behind one, and reserve the topic branch flow for changes that cannot.

Rolling out as a sequence of controlled steps

With deploy and release separated, releasing becomes a sequence of controlled steps rather than a single event.
1

Deploy with the flag off

The code reaches the production environment disabled. Users see no change; the deploy itself carries near-zero release risk.
2

Enable in the verification environment

Turn the flag on in the verification environment first. Problems surface before any user is exposed — the earlier a defect is found, the cheaper it is to fix.
3

Release in the production environment

Enable the flag in the production environment at the chosen timing. If the rollout can be scoped, start with a specific group of users and widen the audience gradually.
4

Roll back by turning the flag off

If the feature misbehaves, disable the flag.
Rollback by flag only works if the old code path still exists and still passes its tests. Do not delete or break the fallback path while the flag is live.
The “is anything wrong?” check at each step is faster and more reliable with automated tests covering the primary user journeys. See E2E Testing for automating the release check against the deployed environment.

Managing flags as temporary

Test both states

A flag doubles the states your code can be in, and both states run in the production environment at some point in the feature’s life. Write tests for each side of the branch:
The off-state test is not optional: it is what guarantees the rollback path keeps working while the feature evolves. See Test Code for what makes these tests trustworthy.

Remove flags that have done their job

A flag is temporary by design. Once the feature has run stably with the flag on and there is no scenario for turning it back off, remove the flag, the dead branch, and the off-state tests in one pull request. Stale flags accumulate into a combinatorial maze of states nobody tests. A trustworthy test suite is what makes this deletion safe to do promptly.

CI/CD

The automated pipeline that makes flag-off deploys repeatable many times a day.

Task Breakdown

Slice features into increments small enough to merge behind a flag.

Pull Requests

Keep each merge small and reviewable.

Test Code

Tests that protect both flag states and make flag removal safe.