Feature Flags: Progressive Delivery Without Release Branches
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
| Type | Lifetime | Example |
|---|---|---|
| Release flag | Days-weeks | New checkout flow |
| Experiment flag | Weeks | A/B test variant |
| Ops flag | Permanent | Kill switch for a third-party integration |
| Permission flag | Permanent | Beta 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:
- Every release flag has an owner and a removal date. Track it in the flag service.
- Remove flags after rollout. A flag that stays past its rollout becomes dead code with two code paths.
- 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
}
}
- 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
- 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
Share this Guide:
More Guides
Model Context Protocol (MCP): Building MCP Servers from Scratch
Build production-grade MCP servers with the TypeScript and Python SDKs. Covers the MCP architecture, stdio and HTTP transports, tools, resources, prompts, and the security model every AI application needs.
16 min readRAG vs Fine-Tuning: When to Use Each for Your LLM Application
Decide between retrieval-augmented generation and fine-tuning with a practical decision framework. Compare cost, latency, freshness, and accuracy, and see working implementations of both approaches.
13 min readGrafana Stack vs SigNoz: Choosing an OpenTelemetry-Native Observability Platform
Compare the Grafana LGTM stack (Loki, Tempo, Mimir) with SigNoz for OpenTelemetry observability: architecture, storage engines, operational overhead, cost at scale, and migration paths.
10 min readLLM Fine-Tuning: LoRA vs QLoRA vs Full Fine-Tuning
Compare full fine-tuning, LoRA, and QLoRA for LLMs with memory requirements, training recipes, and code. Learn which method fits your GPU budget, dataset size, and quality requirements.
14 min readPrompt Injection Defense: Securing LLM Apps Against Jailbreaks, RAG Poisoning, and Tool-Use Exploits
Defend production LLM applications against direct jailbreaks, indirect injection via RAG pipelines, and tool-use exploits with layered defenses, input filtering, and least-privilege tool design.
10 min readContinue Reading
Model Context Protocol (MCP): Building MCP Servers from Scratch
Build production-grade MCP servers with the TypeScript and Python SDKs. Covers the MCP architecture, stdio and HTTP transports, tools, resources, prompts, and the security model every AI application needs.
16 min readRAG vs Fine-Tuning: When to Use Each for Your LLM Application
Decide between retrieval-augmented generation and fine-tuning with a practical decision framework. Compare cost, latency, freshness, and accuracy, and see working implementations of both approaches.
13 min readGrafana Stack vs SigNoz: Choosing an OpenTelemetry-Native Observability Platform
Compare the Grafana LGTM stack (Loki, Tempo, Mimir) with SigNoz for OpenTelemetry observability: architecture, storage engines, operational overhead, cost at scale, and migration paths.
10 min readShip Faster. Ship Safer.
Join thousands of engineering teams using MatterAI to autonomously build, review, and deploy code with enterprise-grade precision.
