CI/CD & DevOps Automation

Feature Flags: Progressive Delivery Without Release Branches

MatterAI
MatterAI
12 min read·

Feature Flags: Progressive Delivery Without Release Branches

Feature flags decouple deployment from release: code ships to production, but features turn on when you decide. This is the foundation of trunk-based development, canary releases, and instant rollback. This guide covers flag types, evaluation architecture, and the discipline that keeps flags from becoming technical debt.

Why Flags

Without flags, a release is a binary event: code is either live or not. With flags:

  • Deploy anytime: merge to main daily; features stay dark until flagged on.
  • Roll back instantly: flip a flag instead of reverting a deploy.
  • Release gradually: roll out to 1%, then 10%, then 100%.
  • Test in production: expose a feature to internal users first.
  • Kill switches: disable a misbehaving integration without a deploy.

Flag Types

TypeLifetimeExample
Release flagDays-weeksNew checkout flow
Experiment flagWeeksA/B test variant
Ops flagPermanentKill switch for a third-party integration
Permission flagPermanentBeta access for specific users

The lifetime matters: release flags must be removed after rollout; ops flags are permanent infrastructure.

Flag Evaluation

The simplest implementation is a config file, but production flags need remote evaluation so you can flip them without redeploying:

// flags.ts — client-side evaluation with a remote flag service
import { FlagClient } from "@acme/flag-client";

const flags = new FlagClient({
  endpoint: "https://flags.acme.io",
  environment: "production",
  // Cache flags locally and refresh every 30s
  cacheTtlMs: 30_000,
});

export async function isEnabled(
  name: string,
  context: FlagContext,
): Promise<boolean> {
  return flags.evaluate(name, context);
}
// Usage — gate a feature behind a flag
const showCheckoutV2 = await isEnabled("checkout.v2", {
  userId: req.user.id,
  email: req.user.email,
});

if (showCheckoutV2) {
  return renderCheckoutV2(req);
}
return renderCheckoutV1(req);

Progressive Rollout

Flags support percentage-based and targeted rollouts:

// Rollout rules — 10% of users, then 100% after 24h
{
  "checkout.v2": {
    "rules": [
      { "audience": { "percentage": 10 } },
      { "audience": { "users": ["internal-team@acme.io"] } }
    ]
  }
}

Sticky bucketing matters: the same user must see the same variant across requests. Bucket by a stable identifier, not by request:

// Deterministic bucketing — same user always gets the same variant
import { createHash } from "node:crypto";

function bucket(userId: string, salt: string): number {
  const hash = createHash("sha256").update(`${salt}:${userId}`).digest();
  return hash.readUInt32BE(0) % 100;
}

const variant = bucket(user.id, "checkout.v2") < 10 ? "v2" : "v1";

Flag Evaluation at the Edge

For latency-sensitive paths, evaluate flags at the edge (CDN) or embed the flag state in the response:

// Server-side rendering — evaluate once, pass down
export async function renderPage(req: Request) {
  const flags = await getFlagsForUser(req.user.id);
  return {
    html: render(flags),
    // Client needs the same flags for hydration
    flags: flags.toJSON(),
  };
}

Flag Hygiene: The Discipline

Flags are debt if they live forever. The rules:

  1. Every release flag has an owner and a removal date. Track it in the flag service.
  2. Remove flags after rollout. A flag that stays past its rollout becomes dead code with two code paths.
  3. Default flags to off in code. The code should be safe if the flag service is unreachable:
// Fail closed: if the flag service is down, use the safe default
export async function isEnabled(
  name: string,
  context: FlagContext,
): Promise<boolean> {
  try {
    return await flags.evaluate(name, context);
  } catch {
    return false; // safe default
  }
}
  1. Test both flag states. CI should run the test suite with flags on and off:
# Run tests with the flag on and off
FLAG_CHECKOUT_V2=true npm test
FLAG_CHECKOUT_V2=false npm test
  1. Audit flags quarterly. Delete flags that are fully rolled out or fully dead.

Trunk-Based Development with Flags

Flags enable the workflow: everyone merges to main, features are hidden behind flags, and releases are boring.

main ──► deploy ──► flags control visibility
  ▲
  │  merge daily, feature dark
  └── feature branch (hours, not weeks)

The rules: branches live hours, not weeks; every in-progress feature is behind a flag; the main branch is always deployable.

Common Failure Modes

  • Flag spaghetti: hundreds of flags with no owner. Fix with hygiene rules and quarterly audits.
  • Nested flags: if (a && b && c) where each is a flag. The combination space explodes; test the combinations you ship.
  • Flags in the wrong layer: flags evaluated in the database layer leak into every query. Evaluate at the request boundary.
  • No kill-switch testing: the rollback flag must be tested, or it will fail when you need it.

Implementation Checklist

  • Choose a flag service (or build a thin one)
  • Classify flags: release, experiment, ops, permission
  • Implement deterministic bucketing by user ID
  • Fail closed when the flag service is unreachable
  • Test both flag states in CI
  • Set owners and removal dates for release flags
  • Audit and remove dead flags quarterly
  • Practice a flag-based rollback in a drill

MatterAI builds frontier AI infrastructure for engineering teams — from inference-optimized models to autonomous coding agents and agentic code reviews.

Explore what we're building:

  • Orbital IDE — Autonomous AI coding agent with background agents and deep codebase memory
  • AI Code Reviews — Agentic pre-commit reviews across GitHub, GitLab, and Bitbucket
  • Axon Models — Frontier-grade reasoning models at 70% lower inference cost

Get started free - https://app.matterai.so


Follow us on X · LinkedIn · GitHub

Share this Guide:

Ship Faster. Ship Safer.

Join thousands of engineering teams using MatterAI to autonomously build, review, and deploy code with enterprise-grade precision.

No credit card requiredSOC 2 Type IISetup in 2 min