Skip to content

Module 1.3: Feature Management at Scale

Discipline Module | Complexity: [MEDIUM] | Time: 2.5 hours

Before starting this module:

  • Required: Module 1.1: Release Strategies — Understanding of progressive delivery, deployment vs release separation
  • Required: Kubernetes Deployments and Services — Ability to deploy and expose applications
  • Recommended: Basic understanding of trunk-based development and CI/CD pipelines
  • Recommended: Familiarity with REST APIs and environment variables

After completing this module, you will be able to:

  • Design a feature flag architecture that decouples deployment from release across microservices
  • Implement feature flag management with gradual rollouts, user targeting, and kill switches
  • Build feature flag lifecycle processes that prevent flag debt and stale configurations
  • Evaluate feature flag platforms — LaunchDarkly, Flagsmith, OpenFeature — against your operational requirements

Hypothetical scenario: a product team ships a new recommendation engine to production after weeks of integration testing. The engine is brilliant — measurably better click-through rates in pre-production. But in production, it saturates a downstream catalog service that nobody load-tested at real traffic volume. The product catalog goes dark. The fix is obvious — revert to the legacy engine — but rolling back requires rebuilding the previous Docker image, running the full CI pipeline, and waiting for health checks across a dozen microservices. Twenty-two minutes pass before the catalog is back. During peak shopping hours, those twenty-two minutes represent a significant revenue loss and a customer trust deficit that takes months to repair.

Had the team wrapped the recommendation engine in a feature flag, recovery would have taken seconds: one API call to the flag service, one configuration change, one toggle. No redeployment. No pipeline. No waiting for health checks. The code for both the new and legacy engines was already deployed — the flag simply routed traffic back to the safe path.

This is not a marginal convenience. It is a fundamental redefinition of operational risk. Feature flags decouple two actions that traditional release engineering treats as inseparable: deploying code to production servers and exposing that code to users. Deployment becomes a routine, low-ceremony event that can happen dozens of times per day. Release becomes a separate, controllable decision with its own safety mechanisms — gradual rollouts, instant kill switches, cohort targeting, and automated circuit breaking. The flag becomes the circuit breaker between your deployment pipeline and your users’ experience.

This module teaches you the durable practice behind feature flag systems: how to design them, operate them at scale, prevent the flag debt that buries unprepared teams, and evaluate the landscape of tools and standards without locking yourself into any single vendor.


At its simplest, a feature flag is an if-statement controlled by external configuration. The flag’s value does not live in the source code. It lives in a configuration service that can be changed at runtime, independently of any deployment. This is the core insight that makes everything else possible.

Consider a recommendation function. Without a flag, the only path available to the application is whatever code is currently deployed. If that code misbehaves, the only recovery mechanism is to deploy different code — a process measured in minutes at best, and in complex microservice topologies, often far longer. With a flag, both the new and legacy code paths are deployed simultaneously, and the flag service determines which path executes:

# Without feature flag — deployment IS release
def get_recommendations(user):
return new_ml_recommendations(user)
# With feature flag — deployment and release are separate concerns
def get_recommendations(user):
if feature_flags.is_enabled("new-recommendations", user=user):
return new_ml_recommendations(user)
else:
return legacy_recommendations(user)

The architectural consequence is profound. Deployment becomes a mechanical operation — push code to servers. Release becomes a dynamic, observable, reversible operation — change a configuration value. The two operations have entirely different risk profiles, different owners, and different rollback mechanisms. This is not a semantic distinction. It is the difference between a twenty-minute outage and a three-second recovery.

The flag service itself is a minimal component at its core: an administration surface where operators toggle flags, and an evaluation API that applications query at runtime. The evaluation result is typically cached locally with a background refresh cycle, so that flag evaluation adds negligible latency to application requests and degrades gracefully when the flag service is temporarily unavailable.

flowchart LR
App[Application<br/>if enabled?] -->|Query| FS[Flag Service<br/>new-reco: true<br/>dark-mode: false]
UI[Admin UI<br/>Toggle on/off] -->|Update| FS

Not all flags are created equal, and treating them as interchangeable is the fastest path to unmanageable technical debt. Pete Hodgson, writing for Martin Fowler’s site, laid out the taxonomy that has become the industry’s reference model: four distinct categories of feature toggles, each with a different lifespan, owner, and purpose. Understanding these categories before you write your first flag prevents the most common failure mode — treating a short-lived release toggle as if it were permanent infrastructure.

Release toggles are the category most teams encounter first. They gate incomplete features so that developers can practice trunk-based development: committing work-in-progress to the main branch daily, deploying it to production behind a disabled flag, and activating the feature only when it is ready. Their lifespan is short — days to weeks — and their owner is the engineering team building the feature. Once the feature is fully rolled out and validated at 100% of traffic, the release toggle and the legacy code path it guards must both be removed. Leaving a release toggle in place after its feature reaches general availability converts it from scaffolding into debt.

Experiment toggles power A/B testing and multivariate experiments. Their lifespan is longer — weeks to months — and their owner is typically a product or data science team rather than the engineers who implemented the underlying feature. An experiment toggle routes different user cohorts to different variants of a feature and feeds the resulting behavioral data into an analytics pipeline that measures conversion, engagement, or whatever metric the experiment is designed to optimize. When the experiment concludes, the toggle and the losing variants are removed, and the winning variant becomes the default — ideally behind its own release toggle if it still requires a gradual rollout.

Ops toggles are long-lived safety mechanisms. They function as kill switches that operations teams can flip during an incident to disable a feature without redeploying, or as circuit breakers that can shed non-critical load when the system is under stress. Unlike release toggles, ops toggles are intended to remain in the codebase permanently. They must be simple — boolean evaluation only, no complex targeting rules — because their primary design goal is reliability under duress. An ops toggle that cannot be evaluated because the flag service is down must default to the safe path. Every critical code path that calls an external dependency should be wrapped in an ops toggle.

Permission toggles encode business logic that determines who has access to what. They control premium feature gating, beta program access, regional availability, and entitlement checks. Their lifespan is permanent — they live as long as the business rule they encode — and their owner is typically a business or product function rather than engineering. Because they are part of the application’s identity and access model, permission toggles should be audited, tested, and treated with the same rigor as any other authorization mechanism. A misconfigured permission toggle that inadvertently exposes a paid feature to free-tier users is a revenue incident.

TypeLifespanWho Controls ItPurposeExample
Release toggleDays to weeksEngineeringGate incomplete features for trunk-based devnew-checkout-flow
Experiment toggleWeeks to monthsProduct/DataA/B testing, measure user behaviorbutton-color-test
Ops toggleLong-livedOperationsCircuit breakers, kill switches, load sheddingdisable-recommendations
Permission togglePermanentBusinessEntitlements, premium features, beta accesspremium-analytics

The critical difference is lifespan. Release toggles should be removed within weeks of reaching 100% rollout. If a flag created as a “temporary” release toggle is still sitting in your codebase six months later, it has become unmanaged technical debt — a dead code path that nobody understands, nobody tests, and nobody dares remove. The operational discipline around flag lifecycle is not optional. It is the single largest determinant of whether a feature flag program succeeds or collapses under its own weight.

Every release toggle should follow a defined lifecycle with explicit phases and a mandatory removal gate. The lifecycle begins with flag creation — the moment a developer registers the flag in the flag service and wraps the new feature code behind it. At this stage the flag is disabled for all users in production while being enabled in development and staging environments so the team can test it. This is the trunk-based development window: incomplete code is being committed to main and deployed to production daily, safely hidden behind the disabled flag.

The next phase is gradual rollout. The flag is enabled for a small percentage of production traffic — typically starting with internal employees or a 1% cohort — and the team watches observability dashboards for error rate, latency, and business metric divergence between the flag-on and flag-off cohorts. Assuming healthy metrics, the rollout percentage increases in steps: 5%, 25%, 50%, then 100%. At each step the team re-validates. If any metric degrades, the rollout pauses or reverses.

Once the flag reaches 100% and has been stable for a defined observation period — typically a week — the feature is considered generally available. This is when the most important phase begins: flag removal. The developer who created the flag files a cleanup ticket that removes the flag evaluation code, the legacy code path it guarded, and the flag definition from the flag service. This is not optional. A flag that reaches 100% but is never removed becomes a zombie — still referenced in code, still consuming evaluation cycles, still confusing every developer who encounters it.

flowchart LR
A[Create Flag<br/>Day 1] --> B[Test off<br/>Day 1-7]
B --> C[Rollout % ramp<br/>Day 7-14]
C --> D[GA 100%<br/>Day 14-21]
D --> E[Remove Flag<br/>Day 21-28]
style E stroke:#ff0000,stroke-width:2px,stroke-dasharray: 5 5
noteE[THIS IS MANDATORY<br/>or the flag becomes debt] -.-> E

Stop and think: What happens if an organization adopts feature flags enthusiastically but fails to enforce the “Remove Flag” phase of the lifecycle for its release toggles? After two years, how many dead code paths exist in the codebase, and what is the organizational cost of determining which ones are safe to remove?


Feature flag evaluation is the act of determining, for a given flag and a given user context, whether the flag returns true or false — or, in the multivariate case, which variant to serve. The evaluation strategy determines how the flag service makes this decision, and the choice of strategy has direct consequences for user experience consistency, system performance, and operational safety.

The simplest strategy is a boolean flag: the flag is either on for everyone or off for everyone. This is appropriate for ops toggles and for the earliest stages of a release toggle’s lifecycle, when the feature is being tested internally but not yet exposed to users.

{
"new-checkout": {
"enabled": true
}
}

Pause and predict: If you roll a feature out to 25% of your users using random probability on every request instead of a sticky attribute, what will the user experience be as they navigate the site? They will see the new checkout on their first page load, the old checkout on their second, and flip unpredictably with every request — an experience that erodes trust, generates support tickets, and contaminates your experiment data.

The solution is percentage rollout with stickiness. The flag service hashes a stable attribute — typically the user ID or session ID — modulo 100, producing a deterministic value between 0 and 99. Users whose hash value falls below the configured percentage threshold see the feature. Users whose hash value falls above it do not. The hash is deterministic: the same user always produces the same hash, so they consistently see the same experience across sessions and devices. When the rollout percentage increases from 25% to 50%, users whose hash values fall in the 25-49 range are newly exposed, but users in the 0-24 range continue to see the feature — nobody loses access they already had.

{
"new-checkout": {
"enabled": true,
"rollout_percentage": 25,
"stickiness": "userId"
}
}

User and group targeting adds another layer of precision. Rules match against attributes in the evaluation context — email domain, country, team, subscription tier — and route specific cohorts to the feature. This enables patterns like “all company employees see the feature immediately, 10% of US and Canadian users see it, everyone else sees the legacy experience.” The evaluation engine processes rules in order, short-circuiting on the first match.

{
"new-checkout": {
"enabled": true,
"rules": [
{
"attribute": "email",
"operator": "endsWith",
"value": "@company.com",
"variant": true
},
{
"attribute": "country",
"operator": "in",
"value": ["US", "CA"],
"rollout_percentage": 10,
"variant": true
}
],
"default": false
}
}

The most operationally sophisticated strategy combines gradual rollout with automated circuit breaking. The flag configuration includes not just the rollout percentage and targeting rules, but also a metrics gate — a threshold on a real observability metric that, if breached, automatically disables the flag without human intervention. This closes the gap between “the on-call engineer noticed the alert” and “the on-call engineer toggled the flag,” reducing recovery time from minutes to seconds.

{
"new-payment-processor": {
"enabled": true,
"kill_switch": true,
"rollout_percentage": 5,
"excluded_regions": ["ap-southeast-1"],
"metrics_gate": {
"metric": "payment_success_rate",
"threshold": 0.995,
"auto_disable_below": 0.99
}
}
}

Where flag evaluation happens matters. Server-side evaluation runs in your backend, where the flag service SDK has access to the full evaluation context — user identity, session data, request attributes — and where flag evaluation rules are never exposed to the client. This is the right choice for business logic, security-sensitive features, and any flag whose targeting rules encode information you do not want visible to end users. Server-side SDKs maintain a local cache of flag configurations refreshed periodically from the flag service, so evaluation latency is sub-millisecond.

Client-side evaluation runs in the browser or mobile application. The flag service delivers a configuration payload to the client — typically a set of flag values pre-evaluated for that specific user — and the client SDK evaluates locally. This is appropriate for UI changes, frontend experiments, and features where the targeting logic does not need to be hidden. The trade-off is that the configuration payload may reveal information about how flags are targeted, and the client’s connectivity determines freshness — a mobile device offline for hours evaluates against stale configuration.

AspectServer-SideClient-Side
WhereIn your backendIn browser/mobile SDK
LatencyNear-zero (local cache)Depends on network
SecurityRules hidden from usersRules visible in payload
TargetingFull context availableLimited to client context
Use caseAPI behavior, business logicUI changes, frontend features

Best practice: Use server-side evaluation for business logic and security-sensitive flags. Use client-side evaluation for UI experiments. Never use client-side evaluation for a flag that gates access to a paid feature or controls a security boundary — the client-side configuration payload is inspectable and tamperable.

A feature flag system must never become a performance bottleneck. If every application request blocks on a network call to the flag service, the flag system adds unacceptable latency and introduces a new single point of failure — the flag service itself — that can take down every application that depends on it. The architecture that prevents this is local caching with background refresh.

The SDK embedded in each application maintains an in-memory cache of all flag configurations relevant to that application. Flag evaluations read from this cache — a pure memory access, sub-millisecond — rather than making a network call. A background thread or polling loop refreshes the cache from the flag service every 10 to 30 seconds, pulling the latest configuration state. This decouples flag evaluation performance from flag service availability: if the flag service goes down, the background refresh simply fails, and the application continues evaluating against the last known good state in the cache. This is graceful degradation, not catastrophic failure.

flowchart TD
subgraph App[Application]
Cache[Local Cache]
Eval[evaluate < 1ms]
Dec[if enabled?]
Cache --> Eval
Eval --> Dec
end
FS[Flag Service<br/>Source of Truth] -->|refresh every 10s| Cache

The cache initialization path deserves careful attention. When an application pod starts, its cache is empty. If the first request that arrives triggers a flag evaluation before the background refresh has completed, the SDK must have a safe fallback — typically the default value specified in the evaluation call itself. The developer writes is_enabled("new-dashboard", default=False), and that default=False is what the SDK returns when the cache is cold. This principle — always provide a safe default, and design the default to be the conservative, known-good path — should govern every flag evaluation in the system.

The architecture of where evaluation runs exists on a spectrum. At one end, evaluation happens inside the application process via an embedded SDK that maintains a local cache. This provides the lowest latency and the strongest resilience against flag service failures, at the cost of requiring the SDK to be integrated into every service. At the other end, evaluation happens at the edge — in an API gateway, a service mesh sidecar, or a CDN edge function — where flags are evaluated before requests reach application code. This centralizes flag logic and reduces SDK integration surface, but introduces network dependency and constrains the richness of the evaluation context.

In the middle sits the server-side relay pattern: a dedicated flag evaluation service runs inside the cluster, and applications call it over localhost or a cluster-internal endpoint. This decouples flag evaluation from the application’s business logic and enables language-agnostic evaluation — a Go evaluation service can serve flags for Python, Node.js, and Java applications alike — but introduces an intra-cluster network call on every evaluation, unless aggressively cached.

The trade-off is a function of your topology. A monolith with a single language runtime benefits most from an embedded SDK. A polyglot microservice architecture with dozens of services in different languages benefits from a centralized evaluation relay with aggressive local caching. Edge evaluation suits CDN-heavy architectures where decisions must be made before the request reaches origin. There is no single correct answer — only a set of trade-offs that must be evaluated against your specific latency budget, language diversity, and resilience requirements.


Landscape snapshot — as of 2026-06. This changes fast; verify against vendor docs before relying on specifics.

The feature flag platform landscape is dynamic. Tools gain and lose features, projects change CNCF maturity levels, and commercial vendors shift pricing and packaging. This section teaches the durable capabilities that matter — the concepts that outlast any single tool’s version — and uses specific platforms as illustrative worked examples, not as endorsements. No “best tool” or “most popular” claims appear here. Evaluate platforms against your operational requirements, not against market perception.

OpenFeature is a CNCF project that defines a vendor-neutral API specification for feature flag evaluation. It was accepted into the CNCF Sandbox in June 2022 and moved to Incubating maturity in November 2023. The project’s thesis is that feature flagging needs the same standardization treatment that observability received from OpenTelemetry: a common API that decouples application code from any single vendor’s SDK, enabling teams to switch providers without rewriting flag evaluation logic.

The architecture is a provider model. Application code imports the OpenFeature SDK for its language and calls a standard evaluation API — get_boolean_value(), get_string_value(), get_integer_value(), get_object_value() — with a flag key, a default value, and an evaluation context. Behind the SDK sits a provider — a plugin that translates the OpenFeature API calls into the specific protocol of whatever flag backend the team has chosen. Swapping from Unleash to LaunchDarkly is a matter of changing the provider configuration. The application code does not change.

from openfeature import api
from openfeature_unleash import UnleashProvider
# Configure the provider (swap this to change platforms)
api.set_provider(UnleashProvider(url="http://unleash:4242/api"))
# Evaluate a flag (same code regardless of provider)
client = api.get_client()
show_new_ui = client.get_boolean_value(
"new-dashboard",
default_value=False,
evaluation_context={"userId": user.id, "country": user.country}
)

Why OpenFeature matters, beyond the obvious vendor-lock-in argument: it also enables teams to run different flag backends in different environments — a lightweight open-source backend in development, the full commercial platform in production — without maintaining separate code paths. It enables gradual migration: adopt OpenFeature with your current vendor’s provider, then swap providers later when requirements change. And it creates a common evaluation context model across languages, so that the targeting attributes you define once are available consistently whether your services are written in Go, Python, Java, or Node.js.

The provider ecosystem includes integrations with LaunchDarkly, Unleash, Flagsmith, Split, CloudBees, and Flagd — a lightweight, CNCF-hosted flag evaluation daemon that can run as a sidecar or a standalone service for teams that want a minimal evaluation layer without a full feature flag platform.

This matrix maps durable capabilities — the things a feature flag system needs to do — to how illustrative platforms implement them. The capabilities are permanent; the platform columns are a snapshot.

CapabilityUnleash (OSS)Flagsmith (OSS)LaunchDarkly (SaaS)Flipt (OSS)
Boolean flagsYesYesYesYes
Multivariate flagsYes (variants)YesYesYes (variants)
Percentage rolloutYes (gradual)YesYesYes
Sticky bucketingYes (userId/sessionId)YesYesYes
Targeting rulesYes (constraints + segments)Yes (traits)Yes (rules engine)Yes (constraints)
Kill switchYes (manual toggle)YesYesYes (boolean)
Flag expiryNo (manual only)No (manual only)Yes (scheduled)No (manual only)
Change audit logYesYesYesYes (via GitOps)
EnvironmentsYesYesYesYes (namespaces)
SDK languagesJava, Node, Go, Python, Ruby, .NET, PHP, Rust, Elixir, Kotlin, Swift, FlutterJava, Node, Go, Python, Ruby, .NET, PHP, Rust, ElixirJava, Node, Go, Python, Ruby, .NET, PHP, Swift, Android, React, FlutterGo, Node, Python, Ruby, Java, .NET, Rust, PHP
OpenFeature providerYes (official)Yes (official)Yes (official)Yes (official)
Self-hostedYes (PostgreSQL)Yes (PostgreSQL)No (SaaS only)Yes (SQLite, PostgreSQL, MySQL)
Streaming updatesNo (polling)No (polling)Yes (SSE/streaming)No (polling)

Unleash is a mature open-source feature flag platform that runs on Kubernetes with a PostgreSQL backend. Its architecture centers on an API server that manages flag configuration, an admin UI for toggling flags and defining strategies, and language-specific SDKs that poll the API server for configuration updates. Unleash’s strategy model is extensible: built-in strategies cover gradual rollout, user ID targeting, IP range, and flexible constraint matching, and teams can implement custom strategies when built-in ones are insufficient. Unleash is widely deployed and has an active community, making it a common starting point for teams building self-hosted feature flag infrastructure.

Flagsmith offers a comparable open-source core with a hosted cloud option. Its differentiating features include identity-based targeting with trait matching — users are assigned flags based on traits like subscription tier, geography, or device type — and a remote configuration capability that lets teams use the same platform for both feature flags and dynamic application configuration. Like Unleash, it provides SDKs across major languages and an official OpenFeature provider.

Flipt takes a deliberately minimal approach. It is a single Go binary with no external database dependency (SQLite, PostgreSQL, or MySQL are options), designed for teams that want a lightweight flag evaluation service without a heavy platform footprint. Flipt stores flag configuration as files in a Git repository and evaluates flags via a REST API or gRPC. Its GitOps-native workflow — flags are defined in version-controlled YAML, and changes flow through pull requests — appeals to teams that already manage infrastructure through Git and want the same audit trail and review process for flag configuration. All four platforms support OpenFeature, so the choice between them is a choice of operational model, not a code-level lock-in decision.


Traditional branching strategies that keep features isolated on long-lived branches create a predictable failure mode. A developer opens a feature branch, works on it for three weeks, and during those three weeks other developers merge dozens of changes to main. The feature branch diverges. The integration at the end — the “merge window” — is a high-ceremony, high-risk event where weeks of accumulated conflicts must be resolved in a single session, often under schedule pressure that discourages thorough testing. Bugs introduced during conflict resolution are discovered in production because the merge itself was never tested in an integrated environment before deployment.

gitGraph
commit
branch feature-x
commit
checkout main
commit
commit
checkout feature-x
commit
checkout main
commit
commit
checkout feature-x
commit
checkout main
merge feature-x type: REVERSE

Feature flags enable trunk-based development by removing the conflict between “I need to commit incomplete code to main” and “I must not expose incomplete code to users.” Developers commit work-in-progress directly to main, wrapped in disabled feature flags. The code is deployed to production on every merge — fully integrated, continuously tested alongside all other changes — but invisible to users. The feature branch disappears. The merge window disappears. Integration is continuous, not a scheduled event.

gitGraph
commit id: "Initial"
commit id: "Add flag (off)"
commit id: "WIP feature 1"
commit id: "WIP feature 2"
commit id: "Enable flag Dev"
commit id: "Enable flag 5% Prod"
commit id: "Enable flag 100% Prod"
commit id: "Remove flag"

Incomplete features are deployed but hidden behind flags. The code below can be sitting on main, deployed to production servers, executing on every request — but the flag evaluation returns false, so users never see the new search engine. The legacy code path continues to serve traffic. The new code path runs in production only when a developer enables the flag in a development environment and tests it against real infrastructure.

# This code is in main, deployed to production, but invisible to users
if feature_flags.is_enabled("new-search-engine"):
results = new_search_engine.search(query) # Work in progress
else:
results = legacy_search.search(query) # What users see

The benefits compound. Merge conflicts become rare because everyone commits to the same branch continuously. Code is integrated and tested in a real production environment from day one, not isolated on a branch where the production topology is simulated. Features can be tested behind flags by internal employees and beta users before the flag is enabled for the general population. None of this is possible without the deployment-release separation that feature flags provide. Feature flags are not an accessory to trunk-based development — they are the enabling mechanism.

The workflow is straightforward but requires discipline at every step. A developer creates a flag in the flag service, configured as disabled by default. They commit feature code behind the flag to main. CI/CD deploys to production — the flag remains off, users see nothing. The developer enables the flag in development and staging environments for testing. QA validates with the flag enabled. The flag is enabled for a small percentage of production users, and the team watches metrics. If metrics are healthy, the percentage ramps up through defined steps. When the flag reaches 100% and has been stable, the developer files a cleanup ticket that removes the flag, the legacy code path, and the dead branch. The removal is as important as the creation — a flag left at 100% is not “done,” it is debt that has not yet been collected.


Operational Discipline: Preventing Flag Debt

Section titled “Operational Discipline: Preventing Flag Debt”

Every team that adopts feature flags enthusiastically eventually confronts the same crisis: hundreds of flags littering the codebase, many at 100% for months, some whose purpose nobody remembers, some referencing dead code paths that have not been tested in a year. This is the flag graveyard, and it is the primary reason teams abandon feature flag programs. It is not a tooling problem — it is a discipline problem. Feature flags without lifecycle management are worse than no feature flags, because the dead code paths they create increase cognitive load, slow down refactoring, and introduce unpredictable behavior when someone accidentally re-enables a flag whose guarded code has rotted.

The Knight Capital disaster of 2012 is the canonical cautionary tale. During a routine deployment, a flag referencing an old trading algorithm — code that had been repurposed years earlier but never removed — was accidentally toggled. The zombie algorithm executed millions of unintended trades in 45 minutes, causing a loss that ultimately forced the firm to be acquired. The root cause was not the flag itself. It was the absence of lifecycle discipline: the flag should have been removed years earlier, and its continued existence in the codebase made the accidental activation possible.

Prevention starts with ownership. Every flag must have a named owner — a team or an individual — who is responsible for its entire lifecycle, from creation through removal. Unowned flags are the first to become graveyard residents. The owner is assigned at flag creation time, recorded in the flag service metadata, and surfaced in regular audits. A dashboard that lists every flag sorted by age, with its owner prominently displayed, creates the social pressure that keeps cleanup from being indefinitely deferred.

Expiry dates make ownership enforceable. Every release toggle gets a mandatory expiry date — typically 30 days from creation — recorded in the flag configuration. When a flag passes its expiry date, the flag service alerts the owning team. If the flag remains past a grace period, automated enforcement escalates: the flag is auto-enabled (or auto-disabled, depending on the team’s policy), and CI/CD pipelines can fail builds that reference expired flags. The expiry mechanism transforms flag cleanup from a best-effort aspiration into a system-enforced requirement.

# Flag configuration in Unleash
name: new-checkout-flow
type: release
created: 2026-01-15
expires: 2026-02-15 # 30 days max
owner: checkout-team
jira_ticket: SHOP-1234

Automated detection closes the loop between code and configuration. A CI check can scan the codebase for flag evaluation calls, cross-reference each flag against the flag service, and fail the build if any flag in the code is stale — defined as at 100% for more than two weeks, or past its expiry date, or owned by a team that no longer exists. This catches the flags that owners forget to clean up and prevents new code from adding dependencies on flags that should already be gone.

#!/bin/bash
# stale-flag-check.sh — Run in CI pipeline
# Extract all flag names from code
FLAGS_IN_CODE=$(grep -roh 'is_enabled("[^"]*")' src/ | sort -u | sed 's/is_enabled("//;s/")//')
# Check each against the flag service
for flag in $FLAGS_IN_CODE; do
STATUS=$(curl -s "http://unleash:4242/api/admin/features/$flag" | jq -r '.stale')
if [ "$STATUS" = "true" ]; then
echo "ERROR: Stale flag '$flag' still referenced in code"
exit 1
fi
done

Team-level constraints create natural pressure toward cleanup. A maximum flag count — “no team may have more than 15 active release toggles” — forces prioritization: to create a new flag, a team must first remove an old one. Some teams formalize this as a “flag tax”: for every flag older than 30 days, the owning team dedicates one hour per sprint to flag cleanup. The specific mechanism matters less than the principle: flag creation must have a cost, and flag removal must have a deadline, or the system drifts toward entropy.

The flag-on path gets tested thoroughly — it is the new code, the feature everyone is building, the path that QA exercises in every test environment. The flag-off path, by contrast, rots silently. It is the legacy code that nobody touches, that gradually diverges from the evolving data models and API contracts around it, until one day someone disables the flag during an incident and discovers that the fallback path crashes on a null pointer that was introduced three months ago in a refactoring that only tested the flag-on path.

Every flag creates two code paths, and both paths must be tested. CI pipelines should run the test suite with flags in both states — on and off — for every release toggle that gates a feature under active development. The flag-off path is not dead code. It is the fallback that your incident response depends on. Treat it accordingly.

Flags as Config: The Change Management Risk

Section titled “Flags as Config: The Change Management Risk”

A subtle but important property of feature flags is that flipping a flag is a configuration change, not a code deploy. This means it bypasses the change management controls that surround deployments — code review, CI validation, staged rollout through environments, approval gates. A flag flip that exposes a feature to 100% of users is a production change with potentially enormous blast radius, executed with a single API call or UI click, often without review.

This is both the power and the danger of feature flags. The solution is not to add heavy process that undermines the speed advantage — it is to recognize that flag configuration changes deserve proportionate controls. Critical flags that gate revenue-sensitive features should require a second approver. Changes to the rollout percentage of a flag that is already serving production traffic should be observable — logged, auditable, and surfaced in the same dashboards that track deployments. The goal is not to make flag changes as slow as deployments. It is to make them as visible.


Stop and think: If a feature flag service goes completely offline, what should the application do when it encounters an if is_enabled("feature-x") check? It should evaluate to a safe default — the value the developer specified in the default= parameter — and continue serving traffic. It must not crash, block, or throw an exception.

A kill switch is an ops toggle designed for one purpose: instant, reliable disablement of a feature during an incident. It differs from a release toggle in its design constraints. A release toggle may have complex targeting rules — percentage rollouts, user segment matching, multi-attribute constraints. A kill switch must be a simple boolean. The evaluation path must be as short and reliable as possible because the kill switch is most likely to be needed when the system is already under stress.

def process_payment(order):
# Kill switch: if payment processor is misbehaving, fall back
if feature_flags.is_enabled("use-new-payment-processor"):
return new_processor.charge(order)
else:
return legacy_processor.charge(order)

Kill switches should default to the safe option — the legacy, proven code path — when the flag service is unavailable. They should be documented in runbooks with explicit criteria: “If payment error rate exceeds 1% for more than two consecutive polling intervals, disable flag use-new-payment-processor.” And they must be tested regularly — actually toggled in a staging environment to verify that the fallback path works, that it handles current data schemas, and that the failover does not introduce its own failure mode. A kill switch that has not been tested is not a kill switch. It is a hope.

Automated Kill Switches and Circuit Breaking

Section titled “Automated Kill Switches and Circuit Breaking”

When you combine feature flags with observability data, you create automated circuit breakers that can disable a feature before a human on-call engineer even notices the alert. A monitoring loop queries Prometheus or your observability backend for a metric — error rate, latency percentile, throughput — and compares it against a threshold. If the metric crosses the threshold, the loop calls the flag service API to disable the flag, then sends an alert so the on-call engineer knows what happened and can investigate.

class AutoKillSwitch:
def __init__(self, flag_name, metric_name, threshold):
self.flag_name = flag_name
self.metric_name = metric_name
self.threshold = threshold
def check(self):
current_value = prometheus.query(self.metric_name)
if current_value < self.threshold:
feature_flags.disable(self.flag_name)
alert.send(f"Auto-disabled {self.flag_name}: "
f"{self.metric_name} dropped to {current_value}")
# Usage
kill_switch = AutoKillSwitch(
flag_name="new-search-engine",
metric_name="search_success_rate",
threshold=0.95
)
# Run every 30 seconds
scheduler.every(30).seconds.do(kill_switch.check)

Circuit breakers, kill switches, and feature flags are complementary mechanisms that operate at different layers of the system. A circuit breaker is automatic, triggered by error thresholds, and protects against downstream dependency failures — it is an infrastructure-level concern. A kill switch is manual, triggered by human judgment, and disables a feature that is technically functioning but producing bad business outcomes — it is an application-level concern. A feature flag is the general mechanism that both can use as their actuator. The distinction matters because the response to an incident should match the incident’s nature: automatic for dependency failures that follow predictable patterns, manual for business logic failures that require human judgment, and always reversible.

MechanismTriggerScopeRecovery
Feature flagManual/scheduledFeature-levelManual toggle
Kill switchManual (emergency)Feature-levelManual toggle
Circuit breakerAutomatic (error threshold)Dependency-levelAuto-reset after cooldown

Flag evaluation results and targeting rules can reveal sensitive information. A client-side evaluation payload that includes flag values for premium features tells an attacker exactly which features are gated. A server-side response that includes detailed targeting rules exposes information about how user segments are defined. Flag configurations should be treated as sensitive operational data. Server-side evaluation keeps targeting rules out of client-visible payloads. Flag evaluation endpoints should require authentication. And flag configuration changes — especially changes to permission toggles that control access to paid features — should generate audit events just like any other authorization change.


When you set a flag to 25% rollout, the flag service must make a decision that is both random (any given user has a 25% chance of seeing the feature) and consistent (the same user always sees the same outcome). Consistent hashing achieves both properties. The flag service hashes a stable attribute — the user ID, a session ID, or any attribute the team designates as the stickiness key — into a numeric value, then takes that value modulo 100. The result is a number between 0 and 99. If the rollout percentage is 25, users whose hash-mod-100 value is less than 25 see the feature; everyone else does not.

User "alice" → hash("alice") = 12 → 12 % 100 = 12 → 12 < 25 → ENABLED
User "bob" → hash("bob") = 73 → 73 % 100 = 73 → 73 > 25 → DISABLED
User "carol" → hash("carol") = 8 → 8 % 100 = 8 → 8 < 25 → ENABLED

When the rollout percentage increases from 25% to 50%, the set of enabled users expands to include those whose hash values fall in the 25-49 range. Users whose hash values are in the 0-24 range — Alice and Carol, in the example — continue to see the feature. Increasing the percentage never removes the feature from a user who already had it, which preserves user experience consistency and prevents the support-ticket-generating behavior of a feature appearing and disappearing.

A disciplined rollout follows a defined schedule with metric validation at each step. The first cohort should be internal employees — the team that built the feature — because they have the context to identify subtle misbehavior and the incentive to fix it before real users are affected. From there, the percentage ramps through increasingly representative cohorts: 1% for a smoke test at minimal scale, 5% for the first real user cohort, 25% for statistical significance on key business metrics, 50% to confirm that the feature scales to half the user base without infrastructure degradation, and finally 100% for general availability. At each step, the team compares error rates, latency distributions, and business metrics between the flag-on and flag-off cohorts. If any metric degrades, the rollout pauses or reverses.

Day 1: Internal employees only (company email targeting)
Day 2: 1% rollout (validate at minimal scale)
Day 3: 5% rollout (first real user cohort)
Day 5: 25% rollout (meaningful scale, watch metrics)
Day 7: 50% rollout (half your users, statistical significance)
Day 10: 100% rollout (GA)
Day 14: Remove flag from code

Monitoring during a rollout is fundamentally a comparison exercise. You are not looking at absolute metric values — you are looking at the delta between the flag-on cohort and the flag-off cohort, because those two cohorts are running against the same infrastructure, the same downstream dependencies, and the same traffic patterns. A difference between them is attributable to the feature itself. If the flag-on cohort shows elevated error rates, increased latency, or degraded conversion, the feature is the cause, and the rollout should be paused immediately.

Flag ON Users Flag OFF Users
Error Rate: 0.3% 0.2% ← Acceptable delta
P99 Latency: 180ms 150ms ← Watch this
Conversion: 4.2% 3.8% ← Feature is working!
Revenue/User: $12.40 $11.90 ← Business impact confirmed

The observability infrastructure must support this comparison. Flag evaluation SDKs should attach the flag state to request traces and metrics, so that dashboards can segment by flag_name:enabled vs flag_name:disabled. Without this segmentation, a rollout is flying blind — you can see that something changed in the aggregate, but you cannot attribute it to the feature or rule out a coincident infrastructure issue.


Pattern 1: Short-Lived Release Toggles with Mandatory Removal. Every release toggle is created with an expiry date, an owner, and a linked cleanup ticket. The flag is removed from both code and configuration within 30 days of reaching 100% rollout. This pattern treats flags as temporary scaffolding, not permanent infrastructure, and prevents the accumulation of dead code paths that make the codebase harder to understand and refactor.

Pattern 2: Kill Switch Default-Safe. Every ops toggle that gates a critical code path defaults to the safe, proven behavior when the flag service is unavailable. The developer writes is_enabled("new-payment-processor", default=False) and the default=False means that a cold cache or a flag service outage routes traffic to the legacy processor, not to a crash. This pattern ensures that the flag system degrades gracefully rather than becoming a new single point of failure.

Pattern 3: Flag State in Observability. Every flag evaluation attaches the flag key and result to the request trace and to relevant metrics. Dashboards can segment by flag state, and alerts can trigger on flag-specific metric divergence. This pattern makes rollouts observable — the team can see, in real time, whether the feature is improving or degrading the metrics that matter.

Pattern 4: Percentage Rollout with Consistent Hashing. Every gradual rollout uses a sticky attribute — user ID, session ID, or organization ID — as the hashing key. Increasing the rollout percentage expands the enabled set without removing the feature from users who already had it. This pattern preserves user experience consistency and prevents the flickering that generates support tickets.

Anti-Pattern 1: The Permanent Release Toggle. A release toggle created for a feature that reached 100% rollout six months ago is still in the codebase. The legacy code path it guards has not been tested in months. The flag evaluation adds runtime overhead with no purpose. This is the most common and most damaging anti-pattern — the flag graveyard in its purest form. The fix is mandatory lifecycle enforcement: expiry dates, automated CI checks, and a team culture that treats flag removal as a completion criterion, not an afterthought.

Anti-Pattern 2: Using Flags for Application Configuration. Mixing feature toggles with runtime configuration — database connection strings, API endpoints, log levels — creates a single namespace where a misconfigured toggle can expose a feature to the wrong audience, and a misconfigured config value can break the application. Feature flags and application configuration should be managed in separate systems with separate access controls and separate change management processes.

Anti-Pattern 3: No Default Value. A flag evaluation call written as is_enabled("feature-x") without an explicit default value. When the flag service is unreachable — during a network partition, a regional outage, or a cold start — the SDK has no guidance on what to return. Some SDKs throw an exception. Others return false silently. Neither is predictable. Always specify an explicit, safe default.

Anti-Pattern 4: Testing Only the Flag-On Path. The flag-on code path — the new feature — is exercised in every test environment. The flag-off path — the legacy fallback — is never tested after the feature is written. When an incident requires disabling the flag, the fallback path crashes because it has not been maintained. Both paths must be tested in CI, and the flag-off path must be treated as production code that must remain functional.

Anti-Pattern 5: Deeply Nested Flags. A code path gated by if flag_A and (flag_B or not flag_C) and flag_D is impossible to reason about during an incident and impossible to test exhaustively. Limit flag nesting to two levels. If the logic is more complex than that, refactor it into a single flag with multiple variants, or extract the decision into a dedicated evaluation function that is tested independently.

Anti-Pattern 6: Flags Without Ownership. A flag created during a hackathon, without an owner, without a JIRA ticket, without an expiry date. Six months later, the flag is at 100% and nobody knows who created it, what it gates, or whether it is safe to remove. Every flag must have an owner assigned at creation time, and that owner must be accountable for the flag’s entire lifecycle.


When choosing an architecture for feature flags, the decision tree starts not with tool evaluation but with operational requirements. The mermaid below models the primary decision path — it is simplified and does not capture every edge case, but it identifies the durable trade-offs that should drive platform selection.

flowchart TD
Start[Need feature flags?] --> Q1{Polyglot<br/>microservices?}
Q1 -->|Yes| Q2{Can you add SDKs<br/>to every service?}
Q2 -->|Yes - SDK route| OpenFeature[Adopt OpenFeature API<br/>provider per environment]
Q2 -->|No - centralized| Relay[Deploy evaluation relay<br/>+ local caching]
Q1 -->|No - single language| Q3{Need SaaS or<br/>self-hosted?}
Q3 -->|Self-hosted| Q4{Heavy or<br/>lightweight ops?}
Q4 -->|Lightweight| Flipt[Flipt or Flagd<br/>GitOps + sidecar]
Q4 -->|Full platform| UnleashFS[Unleash or Flagsmith<br/>PostgreSQL + admin UI]
Q3 -->|SaaS| LD[LaunchDarkly<br/>streaming + managed]
OpenFeature --> Decide{Choose backend}
Decide --> Q3
Relay --> Decide

The decision framework translates into a set of concrete questions that every team should answer before committing to a platform. These questions are durable — the answers change as your architecture evolves, but the questions remain the same.

Decision DimensionQuestions to Answer
Language diversityDo you run services in multiple languages? If yes, OpenFeature’s language-agnostic API becomes more valuable than any single SDK.
Operational ownershipDo you have the capacity to operate a self-hosted platform (PostgreSQL backups, upgrades, monitoring)? If not, SaaS shifts that burden to the vendor.
Latency budgetWhat is your p99 latency budget for flag evaluation? Embedded SDKs provide sub-millisecond evaluation; relay architectures add network latency.
Resilience requirementsWhat happens when the flag service is down? SDKs with local caching degrade gracefully; architectures that call the flag service on every request do not.
Audit and complianceDo you need an audit trail of who changed which flag and when? All major platforms provide this; GitOps-native platforms (Flipt) provide it through Git history.
Experiment sophisticationDo you need A/B testing with statistical analysis, or just feature gating? Platforms with built-in experimentation (LaunchDarkly, Split) may justify their cost for data-driven product teams.

Feature flags and progressive delivery techniques — canary deployments, traffic shifting, blue-green deployments — are complementary mechanisms that operate at different layers of the stack. Feature flags are application-level: they control which code path executes within a running application instance. Traffic shifting is infrastructure-level: it controls which application instances receive traffic. The two mechanisms serve different purposes and have different failure modes, and understanding the boundary between them prevents both duplication and gaps in coverage.

A canary deployment routes a percentage of traffic to a new version of a service — a different Deployment in Kubernetes, a different set of pods, potentially a different container image. This is appropriate when the change involves infrastructure concerns: a new runtime version, a configuration change that affects startup behavior, a database migration that requires the new code to be running before it executes. If the canary pods crash or exhibit elevated error rates, the traffic shift is reversed, and the old pods continue serving.

A feature flag, by contrast, controls behavior within pods that are already running. The same pod can serve both the new and old code paths because both are deployed in the same container image. This is appropriate for application-level changes: a new algorithm, a UI redesign, a behavior change that does not require infrastructure coordination. The flag can be toggled per-user, per-session, or per-request, which provides granularity that traffic shifting cannot match — a canary deployment at 10% exposes 10% of users to the new code; a feature flag at 10% can target specific 10% cohorts based on any attribute.

The two mechanisms are often used together. A canary deployment rolls out a new container image that includes both the old and new code paths, guarded by a feature flag. The canary validates that the new image does not crash, leak memory, or exhibit infrastructure-level problems. Once the canary is healthy, the feature flag controls the application-level rollout — first to internal employees, then to 1% of users, then ramping up through defined thresholds. If the feature misbehaves, the flag is toggled off without rolling back the canary. If the infrastructure misbehaves, the canary is rolled back without touching the flag.

For a deeper treatment of canary deployments and traffic-shifting strategies, see Module 1.1: Release Strategies. For the infrastructure-level implementation of progressive delivery with service mesh tooling, see Module 1.2: Canary Deployments with Argo Rollouts.


  1. GitHub uses feature flags for nearly every change. Their feature flag system, called Flipper, controls thousands of flags simultaneously. Every new feature starts behind a flag, is tested by GitHub employees in a practice called “staff shipping,” then gradually rolled out to a percentage of users. A developer can ship code to production on their first day — safely hidden behind a flag. The system is so embedded in GitHub’s development culture that the company considers it as fundamental as version control itself.

  2. Facebook’s Gatekeeper system evaluates over 10 billion feature flag checks per second across their infrastructure. The system is so critical that it has its own dedicated reliability team. Flag evaluations are cached client-side and refreshed every few seconds, meaning a flag change propagates globally in under 10 seconds to billions of devices. Gatekeeper predates the modern feature flag platforms by years — Facebook built it because they needed it, before the category existed.

  3. Knight Capital lost $440 million in 45 minutes in 2012 because of a deployment that accidentally re-enabled dead code from an old feature flag that was never removed. The flag referenced a trading algorithm that had been repurposed years earlier. When the flag was accidentally toggled during a deployment, the zombie algorithm executed millions of unintended trades. It is one of the most-cited examples of feature-flag technical debt. For the full case study, see Infrastructure as Code.

  4. The CNCF accepted OpenFeature as a Sandbox project in June 2022, and it moved to Incubating maturity in November 2023, recognizing that feature flag standardization is as important as observability standardization. The specification defines a vendor-neutral API with a provider model that lets teams switch between backends — Unleash, LaunchDarkly, Flagsmith, Flipt — without rewriting application code, the same way OpenTelemetry lets you switch between Jaeger, Zipkin, and Honeycomb without changing instrumentation.


MistakeProblemSolution
Never removing release togglesCodebase fills with dead branches and confusing logic that nobody dares touchEnforce expiry dates; block PRs that add flags without matching removal tickets
Using feature flags for configurationMixing feature toggles with app config creates confusion about what controls whatUse flags for features, config maps or dedicated config stores for application configuration
No default value when flag service is downApp crashes or behaves unpredictably during a flag service outageAlways specify a safe default; cache flags locally so the cache survives the outage
Testing only with flags onFlag-off path rots silently and breaks when needed during an incidentTest both paths in CI; the off path is your fallback and must remain functional
Flags without ownershipNobody knows who can remove them or why they existEvery flag must have an owner team and a linked tracking ticket at creation time
Percentage rollout without stickinessUsers randomly flip between experiences on every requestAlways use consistent hashing on a stable attribute like user ID or session ID
Nesting feature flags deeplyif flag_A and flag_B and not flag_C becomes impossible to reason about during an incidentLimit flag nesting to two levels maximum; refactor complex logic into a single flag with variants
Using flags to avoid fixing bugs”We’ll just flag it off” becomes the permanent state of a broken featureFlags hide bugs temporarily; fix the root cause on a deadline — a flag is a tourniquet, not a cure

Scenario: Your team has just merged the final PR for a new payment gateway, guarded by a feature flag new-payment-gateway. Simultaneously, the SRE team has added a flag disable-heavy-reports to prevent the database from crashing during peak hours. How should the lifecycles of these two flags differ?

Show Answer

A release toggle like new-payment-gateway is short-lived — days to weeks — and serves as temporary scaffolding for trunk-based development. Once the payment gateway is fully rolled out to 100% of users and validated against production metrics, both the flag and the legacy code path it guards must be removed from the codebase. Leaving them in place creates dead code that accumulates over time and increases the risk of accidental re-activation, as the Knight Capital incident demonstrated.

In contrast, an ops toggle like disable-heavy-reports is a permanent safety mechanism that is intended to remain in the codebase indefinitely. It functions as a manually-operated circuit breaker that SRE can trigger during peak load to shed non-critical work and protect the database. It must be simple — boolean only, no complex targeting — and default to the safe path so that a flag service outage does not disable the protection it provides.

Scenario: You configure a feature flag to show a redesigned shopping cart to 25% of your users. During the first day, you notice support tickets complaining that the cart “keeps changing back and forth” while users navigate the site. What went wrong with your rollout configuration?

Show Answer

The rollout was configured using random probability on each evaluation rather than consistent hashing tied to a sticky attribute. Without stickiness, the flag service recalculates the 25% probability on every page load, which means a single user’s experience can flicker between the old and new cart as they navigate — sometimes seeing the redesign, sometimes not, unpredictably.

The fix is to configure the rollout with a stickiness key — typically userId or sessionId. The flag service hashes this key into a value between 0 and 99, and users whose hash falls below the 25% threshold consistently see the new cart on every request. When the rollout percentage increases, users who were already in the enabled set remain in it — the hash is deterministic and independent of the percentage threshold.

Scenario: A senior engineer argues that trunk-based development is too risky for your monolith because incomplete features could accidentally be released to production if everyone commits to main daily. How does a feature flag system directly solve this concern?

Show Answer

Feature flags decouple deployment — pushing code to production servers — from release — exposing that code to users. By wrapping incomplete work in a feature flag that defaults to false, developers can merge unfinished code into main and deploy it to production multiple times per day without impacting any users. The code sits on production servers, fully integrated and continuously tested alongside all other changes, but the flag evaluation returns the legacy code path for every real user.

This eliminates the need for long-lived feature branches and the painful merge conflicts they create. It also means that code is integrated continuously from day one, rather than sitting in isolation on a branch for weeks and then being merged in a high-risk integration event where weeks of conflicts must be resolved under schedule pressure.

Scenario: A new engineer joins the company and asks why there are if statements checking flags like xmas-promo-2022 and beta-v2-migration in the core transaction logic. No one on the team knows what these flags do or if it is safe to remove them. What organizational failure led to this, and how do you automate its prevention?

Show Answer

This is the flag graveyard, the direct result of adopting feature flags without lifecycle management. The flags were created as release toggles with the intention of being temporary, but no expiry date was set, no owner was assigned, and no cleanup was enforced. Over time they accumulated, their original context was lost as team members moved on, and now they represent unknown risk — nobody knows whether removing one will break a critical code path.

Automated prevention requires three mechanisms working together. First, every flag gets a mandatory expiry date at creation time — typically 30 days for a release toggle. Second, each flag has an assigned owner who is accountable for its lifecycle. Third, a CI check scans the codebase for flag evaluation calls, cross-references each flag against the flag service to check its age and status, and fails the build if any flag is stale. This bakes cleanup into the development workflow rather than leaving it as a best-effort aspiration.

Scenario: The product catalog service occasionally experiences huge latency spikes when an external inventory API goes down. Separately, a new ML-based product sorting algorithm is generating bizarre recommendations that are confusing customers. Should you use a kill switch or a circuit breaker for each situation?

Show Answer

Use a circuit breaker for the external inventory API and a kill switch for the ML sorting algorithm. The circuit breaker is the appropriate mechanism for the inventory API because the failure pattern is predictable — latency spikes indicate a downstream dependency failure — and the response should be automatic. When the circuit breaker detects that error rates or latency have crossed a defined threshold, it trips and prevents further calls to the failing dependency, protecting the catalog service from cascading failure. When the dependency recovers, the circuit breaker automatically resets after a cooldown period.

The ML sorting algorithm requires a kill switch — a manual ops toggle — because the failure is not technical. The algorithm is functioning correctly from an infrastructure perspective: it returns results, it responds within latency budgets, it does not throw errors. But it is producing bad business outcomes — confusing recommendations — that require human judgment to recognize and human decision-making to respond to. An automated circuit breaker would not detect this failure mode because the technical metrics look healthy.

Scenario: Your cloud provider experiences a regional outage, taking your centralized feature flag service offline for 45 minutes. Your application pods continue running, but they can no longer query the flag service. How do you ensure the application continues serving traffic without crashing or changing user experiences randomly?

Show Answer

The application must rely on local caching and safe defaults to degrade gracefully during the outage. The SDK embedded in each application pod maintains an in-memory cache of flag configurations that is periodically refreshed from the flag service by a background thread or polling loop. When the flag service becomes unreachable, the background refresh simply fails — it does not crash the application — and flag evaluations continue against the last known configuration in the cache. Users see the same features they were seeing before the outage because the cache preserves the pre-outage state.

For pods that start or restart during the outage and therefore have an empty cache, the SDK must fall back to the default value specified in the evaluation call. The developer must have written is_enabled("new-dashboard", default=False) — specifying default=False as the safe, conservative path — so that a pod with a cold cache during an outage serves the legacy experience rather than crashing or throwing an exception. This combination of cached state for running pods and safe defaults for cold starts ensures that the flag service outage does not become an application outage.

Scenario: Your organization has adopted feature flags enthusiastically over the past two years. The flag service now contains over 300 flags. Approximately 200 of them are at 100% rollout and have been for months. You propose a cleanup initiative, but each team that you ask says they are too busy with feature work. How do you break this deadlock?

Show Answer

The deadlock exists because flag cleanup has no natural forcing function — it is always less urgent than feature work, and it will always lose to feature work in prioritization unless the system itself enforces it. The solution is to introduce constraints that make inaction more costly than action. Three mechanisms can break the deadlock.

First, implement automated flag expiry. Any flag that has been at 100% for more than 30 days begins generating alerts to its owning team. After 60 days, the CI pipeline fails builds that reference the flag, preventing new work from depending on it. After 90 days, the flag is auto-removed from the flag service, and the code references become dead code that static analysis will flag. Second, impose a team-level flag cap — no team may have more than 15 active release toggles — so that creating a new flag requires removing an old one first. Third, schedule a recurring “flag debt day” where the entire engineering organization dedicates one day per quarter to flag cleanup, and track team completion publicly. The goal is to shift cleanup from an individual virtue to an organizational expectation backed by automated enforcement.

Scenario: You are evaluating whether to adopt OpenFeature for a new microservice architecture with services in Go, Python, and Node.js. A colleague argues that it adds unnecessary abstraction — “just pick a vendor and use their SDK.” What do you gain from the OpenFeature abstraction layer that justifies its adoption?

Show Answer

OpenFeature provides three durable benefits that a direct vendor SDK integration does not. First, it enables provider portability: you can swap flag backends — from Unleash to LaunchDarkly, from a self-hosted platform to a SaaS — without rewriting flag evaluation code across all your services. In a polyglot architecture with Go, Python, and Node.js services, a vendor migration without OpenFeature requires coordinated SDK changes in three languages across dozens of services. With OpenFeature, it requires changing one provider configuration per service.

Second, it enables environment-specific backends: you can run a lightweight open-source backend like Flipt in development and staging, and a fully featured commercial platform like LaunchDarkly in production, using the same application code. This reduces operational cost and complexity in pre-production environments without creating configuration drift. Third, OpenFeature provides a consistent evaluation context model across languages — the targeting attributes you define once (userId, country, subscriptionTier) are available identically whether the evaluating service is written in Go, Python, or Node.js. Without this standardization, each language SDK may handle context differently, creating subtle behavioral divergence that is hard to debug.


Hands-On Exercise: Deploy Unleash and Toggle a Feature by User ID

Section titled “Hands-On Exercise: Deploy Unleash and Toggle a Feature by User ID”

Deploy Unleash on Kubernetes, create a feature flag with user ID targeting, and demonstrate toggling a feature for specific users without redeploying.

Terminal window
# Create cluster
kind create cluster --name feature-flags-lab
unleash.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: unleash-db
spec:
replicas: 1
selector:
matchLabels:
app: unleash-db
template:
metadata:
labels:
app: unleash-db
spec:
containers:
- name: postgres
image: postgres:15
env:
- name: POSTGRES_DB
value: unleash
- name: POSTGRES_USER
value: unleash
- name: POSTGRES_PASSWORD
value: unleash-pass
ports:
- containerPort: 5432
volumeMounts:
- name: pg-data
mountPath: /var/lib/postgresql/data
volumes:
- name: pg-data
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: unleash-db
spec:
selector:
app: unleash-db
ports:
- port: 5432
targetPort: 5432
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: unleash
spec:
replicas: 1
selector:
matchLabels:
app: unleash
template:
metadata:
labels:
app: unleash
spec:
containers:
- name: unleash
image: unleashorg/unleash-server:6
env:
- name: DATABASE_URL
value: postgres://unleash:***@unleash-db:5432/unleash
- name: DATABASE_SSL
value: "false"
- name: INIT_ADMIN_API_TOKENS
value: "*:*.unleash-admin-api-token"
- name: INIT_CLIENT_API_TOKENS
value: "default:development.unleash-client-api-token"
ports:
- containerPort: 4242
readinessProbe:
httpGet:
path: /health
port: 4242
initialDelaySeconds: 15
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: unleash
spec:
selector:
app: unleash
ports:
- port: 4242
targetPort: 4242
Terminal window
kubectl apply -f unleash.yaml
# Wait for Unleash to be ready
kubectl rollout status deployment unleash --timeout=120s
Terminal window
# Port-forward to access Unleash API
kubectl port-forward svc/unleash 4242:4242 &
# Wait for port-forward
sleep 3
# Create a feature flag
curl -s -X POST http://localhost:4242/api/admin/projects/default/features \
-H "Authorization: *:*.unleash-admin-api-token" \
-H "Content-Type: application/json" \
-d '{
"name": "new-ui-dashboard",
"description": "New dashboard UI with charts",
"type": "release"
}' | jq .
# Enable the flag in the development environment with userIds strategy
curl -s -X POST http://localhost:4242/api/admin/projects/default/features/new-ui-dashboard/environments/development/strategies \
-H "Authorization: *:*.unleash-admin-api-token" \
-H "Content-Type: application/json" \
-d '{
"name": "userWithId",
"parameters": {
"userIds": "user-42,user-99"
}
}' | jq .
# Enable the flag in development environment
curl -s -X POST http://localhost:4242/api/admin/projects/default/features/new-ui-dashboard/environments/development/on \
-H "Authorization: *:*.unleash-admin-api-token" | jq .
sample-app.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-code
data:
server.py: |
from http.server import HTTPServer, BaseHTTPRequestHandler
import urllib.request
import json
import os
UNLEASH_URL = os.getenv("UNLEASH_URL", "http://unleash:4242/api")
API_TOKEN = os.getenv("UNLEASH_API_TOKEN", "default:development.unleash-client-api-token")
def check_flag(flag_name, user_id):
"""Simple flag check against Unleash API."""
try:
url = f"{UNLEASH_URL}/client/features/{flag_name}"
req = urllib.request.Request(url)
req.add_header("Authorization", API_TOKEN)
with urllib.request.urlopen(req, timeout=2) as resp:
data = json.loads(resp.read())
# Simple check - in production use the SDK
if not data.get("enabled", False):
return False
for strategy in data.get("strategies", []):
if strategy["name"] == "userWithId":
user_ids = strategy["parameters"]["userIds"].split(",")
return user_id in user_ids
return data.get("enabled", False)
except Exception as e:
print(f"Flag check failed: {e}")
return False # Safe default
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
# Extract user ID from query param
user_id = "anonymous"
if "?" in self.path:
params = dict(p.split("=") for p in self.path.split("?")[1].split("&"))
user_id = params.get("user", "anonymous")
# Check feature flag
new_ui = check_flag("new-ui-dashboard", user_id)
if new_ui:
body = f"<h1>NEW Dashboard for {user_id}</h1><p>Charts, graphs, and analytics!</p>"
else:
body = f"<h1>Classic Dashboard for {user_id}</h1><p>Simple text view.</p>"
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(body.encode())
HTTPServer(("", 8080), Handler).serve_forever()
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: sample-app
spec:
replicas: 2
selector:
matchLabels:
app: sample-app
template:
metadata:
labels:
app: sample-app
spec:
containers:
- name: app
image: python:3.12-slim
command: ["python", "/app/server.py"]
env:
- name: UNLEASH_URL
value: "http://unleash:4242/api"
- name: UNLEASH_API_TOKEN
value: "default:development.unleash-client-api-token"
ports:
- containerPort: 8080
volumeMounts:
- name: code
mountPath: /app
volumes:
- name: code
configMap:
name: app-code
---
apiVersion: v1
kind: Service
metadata:
name: sample-app
spec:
selector:
app: sample-app
ports:
- port: 80
targetPort: 8080
Terminal window
kubectl apply -f sample-app.yaml
kubectl rollout status deployment sample-app --timeout=60s
# Port-forward the app
kubectl port-forward svc/sample-app 8080:80 &
sleep 2
Terminal window
# User 42 should see the NEW dashboard
curl -s "http://localhost:8080/?user=user-42"
# Output: <h1>NEW Dashboard for user-42</h1><p>Charts, graphs, and analytics!</p>
# User 1 should see the CLASSIC dashboard
curl -s "http://localhost:8080/?user=user-1"
# Output: <h1>Classic Dashboard for user-1</h1><p>Simple text view.</p>
# User 99 should see the NEW dashboard
curl -s "http://localhost:8080/?user=user-99"
# Output: <h1>NEW Dashboard for user-99</h1><p>Charts, graphs, and analytics!</p>

Step 5: Toggle the Flag Without Redeploying

Section titled “Step 5: Toggle the Flag Without Redeploying”
Terminal window
# Disable the flag — no redeployment needed
curl -s -X POST http://localhost:4242/api/admin/projects/default/features/new-ui-dashboard/environments/development/off \
-H "Authorization: *:*.unleash-admin-api-token" | jq .
# Now user-42 sees the classic dashboard
curl -s "http://localhost:8080/?user=user-42"
# Output: <h1>Classic Dashboard for user-42</h1><p>Simple text view.</p>
# Re-enable
curl -s -X POST http://localhost:4242/api/admin/projects/default/features/new-ui-dashboard/environments/development/on \
-H "Authorization: *:*.unleash-admin-api-token" | jq .
# User 42 sees new dashboard again
curl -s "http://localhost:8080/?user=user-42"
# Output: <h1>NEW Dashboard for user-42</h1><p>Charts, graphs, and analytics!</p>

Step 6: Add More Users Without Redeploying

Section titled “Step 6: Add More Users Without Redeploying”
Terminal window
# Get the strategy ID first
STRATEGY_ID=$(curl -s http://localhost:4242/api/admin/projects/default/features/new-ui-dashboard/environments/development/strategies \
-H "Authorization: *:*.unleash-admin-api-token" | jq -r '.[0].id')
echo "Strategy ID: $STRATEGY_ID"
# Update the strategy to add user-7 to the targeting list
curl -s -X PUT "http://localhost:4242/api/admin/projects/default/features/new-ui-dashboard/environments/development/strategies/$STRATEGY_ID" \
-H "Authorization: *:*.unleash-admin-api-token" \
-H "Content-Type: application/json" \
-d '{
"name": "userWithId",
"parameters": {
"userIds": "user-42,user-99,user-7"
}
}' | jq .
# Now user-7 also sees the new dashboard
curl -s "http://localhost:8080/?user=user-7"
# Output: <h1>NEW Dashboard for user-7</h1>
Terminal window
kill %1 %2 2>/dev/null
kind delete cluster --name feature-flags-lab

You have completed this exercise when you can confirm:

  • Unleash is running on Kubernetes with PostgreSQL backend
  • A feature flag was created via the Unleash API
  • User ID targeting correctly showed different UIs to different users
  • Toggling the flag on/off changed behavior without any redeployment
  • Adding new users to the targeting list changed behavior without any redeployment
  • You understand the difference between deployment (code on servers) and release (feature visible to users)


Continue to Module 1.4: Multi-Region & Global Release Orchestration to learn how to coordinate releases across geographies, manage blast radius at planetary scale, and use ring deployments with ArgoCD ApplicationSets.