Feature Flags for Capstone Experimentation
What This Concept Is
A feature flag is a runtime switch that turns a piece of code on or off without redeploying. In a capstone it earns its place in exactly two roles:
- Release toggle: decouple deploy from release. Code ships dark (flag off); you flip it on after smoke tests pass and soak has elapsed.
- Ops toggle (kill switch): a runtime switch for a risky feature if it misbehaves in prod, without a redeploy.
It does not earn its place as:
- a permanent config switch that never moves (that is config, not a flag)
- a user-segmentation engine masquerading as a feature flag (segmentation belongs in a purpose-built tool)
- a substitute for proper authorization (don't use a flag to hide an admin-only feature; enforce it in code)
- an A/B experimentation platform (unless your capstone specifically studies that; real A/B testing is its own module)
Feature flags should be introduced reluctantly and retired aggressively. Martin Fowler's "Feature Toggle" taxonomy separates release toggles (short-lived, tied to a deploy) from permission toggles (may be long-lived, user-scoped); capstones should mostly see the first kind.
Why It Matters Here (In the Capstone)
Flags are deploy-safety tools. A feature behind a flag can ship with code that is wrong without breaking users. A feature without a flag forces you to ship-or-don't. For the capstone, having one or two flags over a semester is evidence of real delivery discipline. Having 40 flags is evidence of process decay -- the codebase becomes a config file with code attached.
Flags also integrate with rollback: a flag flip is a faster rollback than a redeploy for anything flag-gated, and the two mechanisms reinforce each other. Concept 10's trigger list should reference flag flips where applicable.
Concrete Example(s)
Minimal feature flag setup without a SaaS:
// src/flags.ts
import { config } from "./config";
// Retire by: 2026-06-01. Owner: @me.
// Gates /billing/invoices endpoint + new billing page.
export const flags = {
newBillingFlow: config.FEATURE_NEW_BILLING, // boolean from env
} as const;
// in a request handler
if (flags.newBillingFlow) {
return renderNewBilling();
}
return renderLegacyBilling();
To release: change the env var in the secret store from false to true. No deploy needed.
To kill-switch: set it back to false. Observe recovery within the app's cache-TTL window (typically 30-300s).
For a more dynamic flag system (capstone-appropriate when you have 3+ flags), read flags from a small store periodically:
// Re-read flags every 60s from an external store (GCS, S3, or a tiny KV).
setInterval(async () => {
const latest = await fetchFlags();
Object.assign(flags, latest);
}, 60_000);
A retirement scanner, run as a CI step:
# Find any flag whose retirement date has passed.
grep -RE "Retire by: [0-9]{4}-[0-9]{2}-[0-9]{2}" src/ | \
awk -F'Retire by: ' '{ print $2 }' | \
while read -r d _; do
if [[ "$d" < "$(date -I)" ]]; then
echo "EXPIRED: $d"; exit 1
fi
done
Common Confusion / Misconceptions
- "I'll make everything a flag, so I can change anything in prod." That is how you end up with a config file that is the real source of truth instead of the code. The capstone reviewer sees a codebase full of
if (flag) {...}branches and cannot tell what the app actually does today. Each live flag is a conditional branch to reason about. - "A flag replaces a rollback." Sometimes -- but only for features you owned behind the flag from day one. A flag cannot save you from a bug in code paths that were never flag-gated, or from a bad migration, or from a bad Terraform apply.
- "Flag flips are free." They are not. A flag flip is a production change: it alters runtime behavior for real users. Announce the flip, run smoke, and be ready to flip back. Teams that treat flag flips as "free" accumulate incidents that look mysterious in deploy logs because no deploy happened.
- "Keep the dead branch of a retired flag, just in case." Don't. Dead code rots. When a flag retires, delete both branches and the flag itself in one commit. "Just in case" is how codebases grow 20% dead over 18 months.
- "Defaults don't matter." They matter most of all. If the flag store is unreachable, what does the app do? Fail-closed (feature off) is usually safer for new features; fail-open may be required for a kill-switched feature you cannot disable.
How To Use It (In Your Capstone)
- Every flag gets a retirement date and an owner, enforced by a lint rule or a periodic grep in CI.
- Default state in
devshould match the long-term intent (trueif it will stay on;falseif it is a kill switch). - Keep the total count of live flags below 5 for a capstone. If it is growing, you are using flags as architecture -- stop and refactor.
- When a flag retires, delete both branches and the flag declaration in a single commit. No commented-out dead branch.
- Every flag flip is a deploy event: record it in
CHANGELOG.mdwith### Whyand### Risksections, and run smoke after the flip. - Decide each flag's fail-behavior on store unavailability: fail-closed (off) by default; explicit opt-in for fail-open.
- Do not use flags for authorization. Enforce security-critical booleans in code; a leaked flag value must not bypass a permission check.
Flag Flip Is a Deploy Event
Even though flipping a flag does not trigger a new build, it is a production change and should be treated as one:
- announce the flip in
CHANGELOG.mdwith the same### Whyand### Risksections - run the smoke test after the flip, not just after the deploy that introduced the flag
- be ready to flip it back if smoke fails -- the flag is its own rollback
- note the flag flip in
library/raw/rollback-rehearsals/if it is non-trivial
The flag flip was the deploy. Treat it that way.
See also (integrative)
- S9 M04 Cluster 3: Feature flags and dark launches -- canonical treatment; capstone applies the restricted form
- S9 M04 Cluster 3: Progressive delivery and rollback discipline -- flags are the fastest rollback path for flag-gated code
- S9 M04 Cluster 1: Small, frequent, reversible changes -- flags decouple deploy from release
- S8 M04 Cluster 3: Failure modes -- cascading, correlated, gray -- reread on operational kill switches
- S8 M04 Cluster 3: SLIs, SLOs, error budgets -- a flag flip that burns error budget is a signal to flip back
- Martin Fowler: Feature Toggle -- definitive taxonomy and retirement argument
- LaunchDarkly: What is a feature flag? -- vendor-neutral primer
- OpenFeature specification -- vendor-agnostic standard for flag clients
- Google SRE: managing risk with error budgets -- flag flips are error-budget decisions
Check Yourself
- How many flags are live in your codebase right now, and which are closest to their retirement date?
- What is blocking retirement of the oldest live flag, in one sentence?
- If the flag store is unreachable, what is the app's default behavior per flag?
- Was your last flag flip recorded in
CHANGELOG.mdwith### Whyand### Risk? - Which of your flags could be safely replaced by static config (remove the flag, remove the dead branch)?
- Do any of your flags encode authorization decisions? Why?
Mini Drill or Application (Capstone-scoped)
- One ship behind a flag. Pick one feature you will ship in the next two weeks. Gate it behind a flag. Ship the code with the flag
off. After smoke passes and a 24h soak, flip the flag on. Observe for one day. Retire the flag the following sprint. - Retirement audit. List every flag in the repo with its date and owner. Any missing date or owner gets filled in or the flag gets deleted today.
- Flip-as-deploy changelog entry. For your next flag flip, write the
CHANGELOG.mdentry before flipping. If you cannot articulate### Whyand### Risk, the flip is not ready.
Source Backbone
Capstone deployment applies cloud, delivery, and operations material. These books are the source backbone for the delivery decisions.
- Building Secure and Reliable Systems - secure/reliable deployment posture.
- GitHub Actions in Action - workflow automation support.
- Pro Git - release history, tags, and branch discipline.
- The Linux Command Line - shell and deployment automation support.