How Trunk-Based Development Scales with Feature Flags in GitHub

How Trunk-Based Development Scales with Feature Flags in GitHub

How teams keep one shared branch always releasable, merge multiple times a day, and still ship half-finished features safely — by separating "deployed" from "released."

Picture a restaurant kitchen where every chef, no matter how many dishes they’re preparing, works off one single, constantly updated master recipe book chained to the counter — never a personal photocopy taken home for a week. Any change goes straight into that one book, checked immediately by the head chef, so the whole kitchen is always working from the same up-to-date version. But a dish still isn’t served to a customer the moment a step is added to the book — it’s only served once a switch on the pass says “this dish is ready to go out.” Trunk-Based Development is that shared recipe book. Feature flags are that switch on the pass. Together, they let a large team change the book constantly while still controlling, independently, exactly what customers actually receive.

1What Is Trunk-Based Development?

Let’s start with the core discipline before adding feature flags into the picture.

The basic idea

Trunk-Based Development (TBD) is a source-control practice where every developer integrates their changes into one shared branch — usually called main or trunk — extremely frequently, at least once a day, instead of working in isolation on a long-lived feature branch for weeks at a time. The branch itself is kept in a permanently releasable state, meaning it could, in theory, be deployed to production at almost any moment.

Simple Analogy

A long-lived feature branch is like writing a novel chapter alone for six months before finally showing anyone — by the time you share it, the editor’s whole style guide may have changed underneath you, and reconciling the two becomes painful. Trunk-based development is like publishing one paragraph a day to a shared draft, where any style drift is caught and corrected immediately, not six months later.

i
Key Idea

The goal of frequent integration isn’t speed for its own sake — it’s shrinking the size of every merge conflict and every surprise, because the gap between “my code” and “everyone else’s code” never gets large enough to become dangerous.

2The Scaling Problem Trunk-Based Development Creates

Frequent integration solves one problem but immediately creates another.

If every developer merges into main multiple times a day, and main is deployed frequently, then a feature that takes three weeks to build has to live inside that shared branch the entire time — half-built, not ready for real users. Without a way to hide unfinished work, teams are forced back into long-lived branches just to avoid exposing incomplete features, which quietly undoes the entire benefit of trunk-based development in the first place.

!
The Core Tension

Trunk-based development wants code merged constantly. Product management wants features revealed only when they’re finished. These two goals conflict — unless something exists that lets code ship without the feature being visible yet.

That “something” is a feature flag — and it’s the mechanism that makes trunk-based development actually workable at real organizational scale, rather than just a nice idea that falls apart once more than a handful of developers share one branch.

3What Is a Feature Flag?

The single idea that unlocks everything else in this article.

The basic idea

A feature flag (also called a feature toggle) is a conditional check in running code — an “if this flag is on, do the new thing; otherwise, do the old thing” branch — that decides, at runtime, whether a piece of functionality is active. Critically, the flag’s state can be changed without redeploying any code at all, usually through a small configuration dashboard.

Simple Analogy

Wiring a new light fixture into a house doesn’t mean it has to be turned on the moment the electrician finishes. The wiring (the code) can be fully installed and sitting there dormant, and the switch (the flag) is what actually decides whether the room lights up — flipped independently, any time, with no need to touch the wiring again.

Core Concept

Deploy ≠ Release

“Deploying” means the new code exists on production servers. “Releasing” means real users can actually see or use it. Feature flags are what makes these two events independent of each other.

Core Concept

Runtime, Not Build-Time

A flag’s state changes instantly through configuration, not by rebuilding and redeploying the application — which is what makes it dramatically faster to react to than a code change.

4How the Two Ideas Fit Together

graph LR
    A[Developer writes new feature behind a flag] --> B[Merged into main same day]
    B --> C[CI runs tests automatically]
    C --> D[main deployed to production]
    D --> E{Flag On?}
    E -->|No| F[Users see old behavior]
    E -->|Yes for target group| G[Users see new feature]
        
FIG 1 — Code reaches production behind a flag long before it reaches real users, decoupling “merged” from “visible.”

Notice the shape of this: the branch merge happens quickly and constantly (trunk-based development’s job), while the actual exposure to users happens separately and gradually, controlled entirely by the flag (feature flags’ job). Each mechanism solves a different half of the same underlying problem.

Not every flag serves the same purpose — recognizing the type helps decide how long it should live.

Short-lived

Release Flags

Hide an in-progress feature until it’s finished, then get deleted from the code once the feature is fully rolled out and stable — typically lives days to a few weeks.

Short-lived

Experiment Flags

Split users into groups to A/B test a change, and get removed once the experiment concludes and a winner is chosen.

Long-lived

Ops Flags

Act as an emergency kill switch for a risky subsystem — intentionally kept around indefinitely as an operational safety valve.

Long-lived

Permission Flags

Control which tier of customer (e.g. “Enterprise plan”) sees a given feature — tied to the business model, not to a temporary rollout, so it stays indefinitely.

i
Why the Distinction Matters

Treating every flag as permanent is how codebases end up buried in dead conditional branches. Recognizing which flags are meant to be temporary is what keeps the cleanup from being forgotten.

6How GitHub’s CI Pipeline Enforces “Always Releasable”

Trunk-based development’s core promise — that main is always deployable — has to be mechanically enforced, not just hoped for.

GitHub Actions runs automatically on every push and every pull request targeting main, executing the build, the automated test suite, and any linting or security checks before a change is allowed anywhere near the shared branch. Combined with a branch protection rule or ruleset requiring those checks to pass — and typically requiring at least one reviewer’s approval — this is what turns “we try to keep main releasable” from a team promise into something the platform actually guarantees.

1

Developer opens a small pull request

The change is deliberately kept small, often wrapping any incomplete behavior behind a new flag defaulted to off.

2

GitHub Actions runs the required checks

Automated tests and builds confirm the change doesn’t break anything, regardless of whether the flag is currently on or off.

3

A reviewer approves

A human confirms the change is sound, even though the feature itself may not be visible to any real user yet.

4

The pull request merges into main the same day

The branch never accumulates weeks of undiscovered drift from everyone else’s work.

Even once a flag is turned on, it rarely goes to 100% of users at once.

GitHub Environments let a deployment pipeline define distinct stages — such as staging, then a canary slice of production, then the rest of production — each with its own required approvals and protection rules. A team commonly combines this with a feature flag that starts targeting a tiny internal group, widens to a small percentage of real users, and only reaches everyone once the earlier rings show no problems.

graph TD
    A[Flag: Internal team only] --> B[Flag: 5 percent of users]
    B --> C[Flag: 25 percent of users]
    C --> D[Flag: 100 percent of users]
    D --> E[Flag deleted, code becomes permanent]
        
FIG 2 — A typical ring-based rollout: the same deployed code is exposed to progressively larger audiences purely by adjusting the flag.
Simple Analogy

It’s the difference between opening a new restaurant to your family for a trial dinner first, then a few close friends, then a soft public opening on a quiet weekday, before finally opening fully on a busy Saturday — the kitchen (the code) doesn’t change between stages, only how many people are let in the door.

8Feature Flags as an Emergency Kill Switch

Perhaps the most underrated benefit of feature flags has nothing to do with gradual rollout at all.

If a newly released feature starts causing errors or performance problems in production, a team using feature flags can simply turn the flag off — reverting user-visible behavior in seconds, through a configuration change, with zero code deploy and zero risk of introducing a new bug during a rushed rollback. Compare that to a team without flags, who would need to revert a commit, wait for CI, and redeploy — a process that takes meaningfully longer during an active incident.

i
Why This Matters at Scale

The larger and more frequently a team deploys, the more valuable an instant, code-free rollback mechanism becomes — it’s the safety net that makes deploying constantly feel low-risk instead of nerve-wracking.

9Where Flag State Actually Lives

A common beginner question: is a feature flag just a variable in the code?

Not quite — the whole point is that the flag’s current value is stored outside the deployed code, typically in a dedicated feature-flag management service or a lightweight configuration store the application checks at runtime. The application code only contains the conditional logic (“if flag X is on, do Y”); the actual on/off/percentage decision lives in a system a non-engineer, like a product manager, can safely adjust without ever touching a Git repository.

Lives in Git

The Conditional Logic

The code branch that checks the flag and decides which behavior to execute — this part goes through the normal pull request and review process.

Lives Outside Git

The Flag’s Current Value

The actual on/off/percentage setting — changeable instantly through a dashboard, with no pull request, no CI run, and no deploy required.

10Monitoring & Observability

A flag that no one is watching is just an untracked risk sitting in production.

Mature teams tag their monitoring and error-tracking data with which flags were active for a given request, so if error rates spike, they can immediately see whether the spike correlates with a specific flag being turned on for a specific group — turning “something broke, and we don’t know why” into “flag X caused this, and we can turn it off right now.”

Seconds
to disable a flag versus minutes to redeploy
1
shared branch, always kept releasable
Multiple
merges to main per developer, per day

11Design Patterns & Anti-patterns

Pattern: Flag Every Incomplete Feature by Default-Off

Any pull request that introduces user-visible but unfinished behavior wraps it in a flag defaulted to off, so it can merge into main immediately without waiting for the whole feature to be complete.

Pattern: Schedule Flag Cleanup as Part of “Done”

A release flag isn’t considered fully finished until the old code path and the conditional check are both deleted — not just when the new behavior reaches 100% of users.

ANTI-PATTERN-01 Avoid
Problem

Letting release flags accumulate indefinitely after their feature has already fully shipped to everyone.

Why It’s Harmful

Every unremoved flag is a permanent branch point in the code — testing has to cover both states forever, and new engineers waste time figuring out whether an old flag still matters.

Correct Approach

Treat flag removal as a required step of the feature’s definition of done, and periodically audit for flags that have been at 100% for months with no cleanup ticket.

ANTI-PATTERN-02 Avoid
Problem

Using a long-lived feature branch “just this once” for a large feature, because it feels safer than merging unfinished work into main.

Why It’s Harmful

It recreates exactly the integration-pain problem trunk-based development exists to solve — the branch drifts further from main every day it stays open, guaranteeing a painful merge later.

Correct Approach

Break the feature into small, mergeable pieces behind a flag instead, so integration happens continuously rather than in one large, risky merge at the end.

12Best Practices & Common Mistakes

Best Practices

  • Keep pull requests small enough to merge into main at least once a day
  • Default every new flag to “off” so merging never accidentally exposes unfinished work
  • Use GitHub Environments and required approvals to gate progressive rollout stages
  • Tag monitoring data with active flags so incidents can be traced back quickly
  • Delete a flag and its old code path as soon as the rollout is complete and stable

Common Mistakes

  • Keeping a long-lived branch open for weeks to “protect” main from unfinished work
  • Never auditing which flags are still actually in use
  • Rolling a flag out to 100% of users in one step instead of gradually
  • Forgetting that a flag’s state, not just its code, needs its own change history and ownership

13Real-World Shapes of This Problem

Large-Scale Consumer Apps

High-Traffic Products

Rely heavily on ring-based rollout through GitHub Environments, exposing a new feature to a small percentage of traffic first so any performance regression is caught before it affects everyone.

Regulated Industries

Finance & Healthcare

Lean on permission flags tied to compliance tiers, ensuring a feature is only visible to the specific customer segments legally or contractually allowed to use it.

Fast-Moving Startups

Small, High-Velocity Teams

Use trunk-based development mainly for the speed benefit — merging multiple times a day — with flags kept intentionally lightweight and short-lived.

14Advantages & Trade-offs

Advantages

  • Small, frequent merges dramatically shrink merge-conflict size and risk
  • Deploying and releasing become independent, lower-risk decisions
  • Instant, code-free rollback during an incident
  • Gradual rollout catches problems before they reach every user
  • Product and engineering can decouple “code is ready” from “business wants it visible”

Disadvantages / Trade-offs

  • Requires strong CI discipline — a weak or slow pipeline undermines the whole approach
  • Uncleaned flags accumulate as permanent code complexity over time
  • Testing effectively means testing multiple flag-state combinations, not just one code path
  • Introduces a second system (the flag store) that itself needs monitoring and access control
“Deploying is what engineers control; releasing is what a flag decides.”

15Frequently Asked Questions

Q1Does trunk-based development mean there are no branches at all?

Not quite — short-lived branches for individual pull requests are still normal and encouraged. What’s avoided is a branch that stays open and diverges from main for weeks at a time.

Q2Can a feature flag be toggled without any deploy at all?

Yes — that’s the entire point. The flag’s value lives outside the deployed code, in a configuration store, so flipping it doesn’t require a new build or deployment.

Q3How does GitHub Actions relate to feature flags specifically?

GitHub Actions doesn’t manage flag state itself — its role is enforcing that whatever gets merged into main, flagged or not, passes tests and review, which is what makes it safe to merge unfinished work behind a flag in the first place.

Q4What happens if a pull request behind a flag is never actually finished?

The flag simply stays off indefinitely, and the dormant code sits harmlessly in the codebase until either the feature is resumed or the flag and its code are deliberately removed.

Q5Is this approach only useful for large teams?

No — even a small team benefits from smaller, safer merges and instant rollback, though the scaling benefits (avoiding painful long-branch merges, ring-based rollout across many users) become most visible as the team and user base grow.

16Summary and Key Takeaways

Trunk-Based Development keeps one shared branch always releasable by insisting on small, frequent merges instead of long-lived, drifting feature branches. On its own, that discipline creates a problem: unfinished work has nowhere to hide. Feature flags solve exactly that problem, separating the moment code is deployed from the moment it’s actually released to users — and in doing so, they let a team merge constantly, roll out gradually, and roll back instantly, all without a single extra deploy.

Key Takeaways

  • Trunk-based development shrinks integration pain — frequent, small merges into one shared branch avoid the large, risky merges that long-lived branches cause.
  • Feature flags decouple deploy from release — code can exist in production and remain completely invisible to users until a flag says otherwise.
  • Flags come in different lifespans — release and experiment flags should be deleted once their purpose is served; ops and permission flags are meant to stay.
  • GitHub Actions and branch rules enforce the “always releasable” promise — automated checks and reviews gate every merge into main.
  • GitHub Environments enable ring-based rollout — the same deployed code can be progressively exposed to larger audiences.
  • A flag is also an instant kill switch — reverting user-visible behavior in seconds, with no code deploy, during an incident.
  • Flag cleanup is part of “done,” not optional housekeeping — leaving old flags in place is the single biggest long-term cost of this approach.