Module 2: Feature Flags
What a feature flag (really) is: anatomy and where it lives
Description
The previous lesson proved the property that makes a feature flag different from any if: the value that decides its behavior lives outside the code. But "outside" is still vague — outside where, exactly, and in what shape? This lesson answers both questions: the shape a real flag takes (the fields it has, beyond the simple enabled you used in lesson 2) and the place where several flags coexist — a registry — from which the code reads them on every request.
Connection to the module. This lesson builds the exact data structure lessons 4 and 5 are going to extend: today the flag has name and enabled; lesson 4 adds rolloutPercent for gradual exposure; lesson 5 uses that same enabled as a kill switch. Nothing that follows in this module makes sense without the anatomy you see today.
An analogy: the labeled switch, on the breaker panel
A single wall switch, alone, is useful for one room. But a whole house doesn't have a loose switch per room with no order — it has a breaker panel, where every circuit is labeled ("living room," "kitchen," "outdoor light"), and anyone who needs to work on it — a new electrician, a tenant who's never lived there — can open the panel, read the labels, and immediately understand what each one controls without having to trace wires through the whole house.
A real feature flag lives the same way: it isn't a loose true/false value tucked away somewhere in the code, it's a named entry in a registry — the panel — alongside other flags, each with its own label and its own state. Any Mercado engineer, even one who never touched recommendations, can open that registry and understand, without reading a single line of the feature's code, what the flag controls, who's responsible for it, and what state it's in right now.
Worked example: Mercado's flag registry
Let's build a registry with two real Mercado flags, and a function that looks one up by name — exactly as production code would on every request:
// Flag registry: where they really live (config/DB/service), not in the code.
const flagRegistry = [
{ name: 'recommendations', description: 'recommended products carousel on the product page', enabled: true, rolloutPercent: 10, owner: 'squad-discovery', type: 'release' },
{ name: 'checkoutVariantB', description: 'alternate checkout layout, under A/B test', enabled: true, rolloutPercent: 50, owner: 'squad-checkout', type: 'experiment' },
];
function getFlag(name, registry) {
const found = registry.find((f) => f.name === name);
if (!found) throw new Error('flag not found: ' + name);
return found;
}
console.log('=== Anatomy of the "recommendations" flag, read from the registry ===\n');
const rec = getFlag('recommendations', flagRegistry);
Object.entries(rec).forEach(([key, value]) => console.log(' ' + key.padEnd(16) + '= ' + value));
console.log('\n=== Changing rolloutPercent in the registry, without touching code ===');
console.log('Before: rolloutPercent=' + rec.rolloutPercent);
rec.rolloutPercent = 25; // in production this happens in a flag service's UI or a database row -- not in a code editor
console.log('After: rolloutPercent=' + rec.rolloutPercent);
console.log('\nno application function got redeployed between these two lines.');
What to expect. Running the file with Node, the output is exactly this:
=== Anatomy of the "recommendations" flag, read from the registry ===
name = recommendations
description = recommended products carousel on the product page
enabled = true
rolloutPercent = 10
owner = squad-discovery
type = release
=== Changing rolloutPercent in the registry, without touching code ===
Before: rolloutPercent=10
After: rolloutPercent=25
no application function got redeployed between these two lines.
Notice the six fields the flag has, beyond the binary enabled from the previous lesson. name is the unique identifier the code uses to request it (getFlag('recommendations', ...)) — it has to be stable, because changing the name breaks every place in the code reading it. description exists so someone who never wrote the feature can understand, in one sentence, what it controls — think of the breaker panel's label. owner answers a question that becomes critical in a real incident: if something goes wrong with recommendations at three in the morning, which team gets woken up? type — which you're going to develop in depth in lesson 6 — says whether this flag is temporary or long-lived, and therefore how urgent it is to remove it later. And rolloutPercent, the field the example's second half changed live, is exactly the piece lesson 4 is going to put to real work.
Why the registry, not the loose flag
Lesson 2's example had a single, isolated flag. Today's has a registry: an array (in production, usually a table or a service) where several flags coexist, each searchable by its name. That difference matters for a very concrete reason: Mercado doesn't have one feature flag — it has, at any given moment, dozens, each controlling a different feature, at different stages of its lifecycle (recommendations just starting its rollout at 10%; checkoutVariantB running an experiment at 50%). Without a central registry, each flag would end up living in a different place in the code, with its own way of being read — exactly the mess an unorganized breaker panel would produce in a house with twenty circuits. The registry is what lets getFlag('recommendations', flagRegistry) work the same way as getFlag('checkoutVariantB', flagRegistry), without the code using them needing to know anything special about each one.
Worth noting, so as not to get ahead of ourselves: today's example still uses enabled as a simple binary value — the registry defines that a rolloutPercent exists, but isn't yet using it to decide, user by user, who sees the feature. That logic — the one that turns rolloutPercent: 10 into a real decision for each buyer — is exactly lesson 4's job.
Common mistakes
Storing the flag in a global code variable, instead of a real external registry. What happens: someone declares let recommendationsEnabled = true; in some file in the project, and although it can technically be changed without touching the business logic, changing that variable still requires editing the source code and deploying. Why it happens: a variable in the code feels "separate" from the logic that uses it, and it's easy to confuse that superficial separation with the real separation a feature flag requires — living somewhere the deployed code can read without recompiling. How to spot it: lesson 2's question is still the right test — can I change this value with no deploy at all? If the answer involves touching a .js or .py file, it's not a real external registry. How to fix it: as in today's example, the registry lives outside the business logic file — in production, in a database or a dedicated service the code queries, not in a variable declaration inside the same module.
Skipping owner and description because "we already know what the flag does." What happens: the team that creates recommendations knows exactly what it controls and who's responsible, so it doesn't bother filling in those fields — until, months later, another engineer (or the same one, who's already forgotten) finds the flag in the registry with no clue what it does or who to ask. Why it happens: the context is fresh in the mind of whoever creates the flag, and filling in metadata feels like extra work with no immediate benefit. How to spot it: if someone asks "what does this flag do?" and the only way to find out is reading the code that consumes it (instead of reading the registry), the metadata failed its purpose. How to fix it: treat description and owner as a mandatory part of creating any flag, not optional documentation — they're, literally, what makes the registry readable by someone who didn't write the feature, exactly like the breaker panel's labels.
Assuming changing rolloutPercent in the registry has an immediate effect, without having built the logic that reads it yet. What happens: the team changes rolloutPercent from 10 to 25 in the registry, like in today's example, and expects 25% of users to automatically start seeing recommendations — but the field, by itself, does nothing; a function is needed that reads it and decides, user by user. Why it happens: this lesson's example shows the data changes without a deploy, and it's easy to confuse that with the data acting without a deploy. How to spot it: if changing rolloutPercent in the registry doesn't move any real number of exposed users, the decision piece is missing. How to fix it: rolloutPercent is data — lesson 4 builds the function, isEnabled(userId, flag), that actually turns it into a per-user decision. A field in an object and the logic that interprets it are two different things, and both are needed.
Exercises
Exercise 1 — Add a flag to the registry. Mercado's logistics team needs a new flag, deliveryEtaV2, for an improved estimated delivery time algorithm, not yet activated for anyone, owned by the squad-logistics team, at the initial rollout stage. Write the full object, with this lesson's example's six fields.
See solution
{
name: 'deliveryEtaV2',
description: 'improved estimated delivery time algorithm',
enabled: false,
rolloutPercent: 0,
owner: 'squad-logistics',
type: 'release',
}
Notice enabled: false and rolloutPercent: 0 together describe "not yet activated for anyone" — you don't need to pick one field over the other to express this; both, together, leave the intent unambiguous. type: 'release' because, as you'll see in lesson 6, a new algorithm that eventually reaches 100% and gets retired is exactly the pattern of a release-type flag.
Exercise 2 — Find the bug. A teammate writes this function and complains that getFlag('CheckoutVariantB', flagRegistry) throws the error 'flag not found: CheckoutVariantB', even though the flag does exist in this lesson's example registry. What's going on?
See solution
getFlag() uses f.name === name — an exact, case-sensitive comparison. The flag in the registry is named 'checkoutVariantB' (lowercase "c"), and the lookup was done with 'CheckoutVariantB' (uppercase "C") — since JavaScript is case-sensitive for strings, 'checkoutVariantB' === 'CheckoutVariantB' evaluates to false, and the function finds no match. The fix isn't in getFlag()'s code — it's working exactly as it should — it's in using the exact name as stored in the registry. This is, in practice, one more reason for name to be a consistent, well-documented value, not something each person types from memory.
Exercise 3 — Explain the registry without using the word "flag." In two or three sentences, explain to someone new on the team what flagRegistry is and what getFlag() is for, without using the word "flag" or "feature flag." You can use the breaker panel analogy.
See solution
An example answer: "It's like a big house's breaker panel: instead of every switch loose, hidden in a different spot, there's one single place where they all are, each with its label — what it controls, who's responsible — and its current position. When someone needs to know or change the state of something specific, they don't have to search the whole house: they go to the panel, find the right label, and there it is." The central idea, without the technical vocabulary: a single, organized, named place for every switch, instead of scattered, undocumented switches.
Summary and next step
In this lesson you built the complete anatomy of a real feature flag — name, description, enabled, rolloutPercent, owner, type — and saw how several flags coexist in a central registry, searchable by name, exactly as a production system would. Running the example, you confirmed that changing a registry field (rolloutPercent: 10 to 25) doesn't require deploying any application function — lesson 2's separation, now with a concrete shape.
Before moving on you should be able to: name a flag's six fields and explain what each one is for; explain why flags live in a central registry instead of scattered through the code; and distinguish between "the data changed" (the registry) and "the data produced an effect" (the logic still to be built).
Lesson 4 builds exactly that missing logic: isEnabled(userId, flag), the function that reads rolloutPercent from the registry and decides, for each Mercado buyer, whether they see recommendations — with a deterministic hash that guarantees the same buyer always gets the same answer.
Resources
- Pete Hodgson (with Martin Fowler), "Feature Toggles (aka Feature Flags)" — martinfowler.com/articles/feature-toggles.html. The section on storing toggle state describes exactly why a feature flag needs to live in a centralized, queryable place, beyond a simple loose value.
- Google SRE Workbook, Chapter 16, "Canarying Releases" — sre.google/workbook/canarying-releases. Explicitly mentions feature flag/experiment frameworks as the mechanism that "allows decoupling feature rollout from a binary release" — the same idea this module develops with code.