The system design question
Design a feature flag system for Stripe's engineering organization. Engineers can turn features on or off, roll them out to a percentage of merchants or users, and keep explicit include and exclude lists. It has to work across all of Stripe's backend services.
Agree on the scope (5 minutes)
Say up front what you're covering.
| We'll design | We'll skip |
|---|---|
| Creating flags and their rules | Measuring experiment results |
| Checking a flag inside any backend service | The admin UI design |
| Getting flag changes out to every service quickly | Flags in browsers and mobile apps |
| A history of who changed what | General config that isn't on or off |
Then agree on what "good" means:
- Nearly free to check: a flag check is an in-memory lookup, well under a millisecond, with no network call.
- Keeps working: if the flag service goes down, every service keeps using the last flags it saw.
- Quick to change: when someone flips a flag, every service sees it within a few seconds.
- Stable: a merchant in a 10% rollout gets the same answer on every request and in every service.
- Safe: every change is recorded, and a bad change can be undone in one click.
Rough numbers (5 minutes)
These are assumptions, but reasonable ones: 5,000 active flags, 500 services running on about 20,000 processes, a million flag checks a second across all of them, and a few hundred flag changes a day.
- ~5K
- active flags
- ~20K
- processes that check flags
- ~1M
- flag checks per second
- ~10 MB
- for every flag and rule
| Question | Rough math | What it means |
|---|---|---|
| Can each check call a flag service? | 1M calls a second, each adding about 1 ms | No. That's a huge service to run, and it'd slow down every request. Check flags in memory |
| Does every flag fit in memory? | 5,000 flags × about 2 KB | About 10 MB. Every process can hold a full copy |
| What if we resend everything on each change? | 300 changes × 20,000 processes × 10 MB | About 60 TB a day. So send only the flags that changed |
The overall design (10 minutes)
- An engineer creates or edits a flag through the admin API. The flag store saves the new rules and bumps a global version number.
- The change is published to a change feed, and a fresh full snapshot of all flags is written to file storage now and then.
- A small agent on each host keeps an open connection to the flag service. When the version moves, it pulls the changed flags and writes them to local disk.
- Each service uses a small library that loads the local copy into memory and answers is_enabled(flag, merchant) with no network call.
- The library counts how often each flag is checked and reports it in the background, so we can spot flags nobody uses any more.
What the apps need to do
| Action | Who does it | What's sent |
|---|---|---|
| Create or edit a flag | Engineer | Flag name, on or off, rollout percentage, include and exclude lists, and a reason |
| Kill switch | Engineer | Flag name. Turns it off everywhere at once |
| Get all flags | Host agent, on startup | Nothing. Gets the latest snapshot and its version |
| Watch for changes | Host agent | The version it has. Gets back any flags changed since |
| Check a flag | Service code, in memory | Flag name, merchant ID, and user ID if there is one |
| See a flag's history | Engineer | Flag name. Gets every change, who made it and why |
What we store and where
| Data | Where it goes | Why |
|---|---|---|
| Flags and rules | Relational database | Small, changed rarely, and edits need to be all or nothing |
| Change history | Append-only table in the same database | Who changed what and when, and lets us roll back |
| Full snapshots | File storage, one per version | New hosts can start from one file instead of asking for every flag |
| Local copy | Each host's disk, plus memory in each process | Checks stay fast, and survive the flag service being down |
Step 1: Checking a flag (10 minutes)
The check runs in order, and the first thing that matches wins: if the flag's switched off, return false. If the merchant's on the exclude list, false. If they're on the include list, true. Then the percentage rollout. If nothing matched, return the flag's default.
For the percentage, hash the flag name and the merchant ID together into a number from 0 to 9,999. If that number is below the rollout (500 for 5%), the flag's on. The same merchant always lands in the same bucket, so they get the same answer every time, in every service, without storing anything per merchant.
Two details matter here. Mixing in the flag name means different flags don't all pick the same 5% of merchants first. And when you go from 5% to 10%, everyone below 500 stays below 1,000, so nobody who had the feature loses it.
- Include and exclude lists load into a hash set, so checking one is a single lookup.
- Some lists get huge, like 'every merchant in Japan'. Make those named groups that live once and get shared by many flags, instead of copying 100,000 IDs into each one.
- If code asks for a flag that doesn't exist, maybe a typo, return the default written in the code and log a warning.
Step 2: Getting changes out, and surviving outages (15 minutes)
Each host runs one agent rather than every process talking to the flag service. That cuts 20,000 connections down to a few thousand. The agent sends the version it has and holds the request open until something changes, then gets just the changed flags. Changes land in a second or two.
The agent writes everything to local disk, and the library in each process reads from there. That's caching at every layer, and it's what keeps things working in an outage. If the flag service dies, agents keep the last copy and services keep checking flags as normal. They just don't see new changes until it's back.
That's a deliberate choice of availability over consistency, which is the CAP trade-off. For a few seconds after a change, one service might see the new value and another the old one. For flags that's fine. Make sure people know that, and don't use flags for anything that has to change everywhere at exactly the same moment.
Starting up when things are broken
A brand new host starts by downloading the latest snapshot from file storage, which is simple and easy to keep up even when the flag service isn't. If even that fails, the library uses the defaults written in code. Every flag check in code has a default, and code review should check that it's the safe choice, usually 'off'.
Lots of hosts reconnecting at once
When the flag service restarts, thousands of agents reconnect together. Put a few relay servers between them, each serving a share of agents. Agents wait a random few seconds before reconnecting so they don't all arrive at once.
Step 3: Making changes safe (5 minutes)
A flag change can break production as easily as a code deploy can. So treat it like one: every change needs a reason, gets recorded with who made it, and creates a new version you can roll back to.
For risky flags, ramp up in steps, like 1%, 5%, 25% then 100%, and watch error rates between steps. Flags tagged as critical can require a second person to approve the change.
Old flags pile up. Use the check counts each library reports to find flags that have been at 100% for a month, and nag their owners to delete them.
Choices you should be able to defend
| We chose | Instead of | Because | We'd reconsider if |
|---|---|---|---|
| Check flags in memory | Call a flag service on each check | Checks are nearly free, and outages don't break them | Flags had to change everywhere instantly |
| Hash the merchant into a bucket | Store each merchant's assignment | Nothing to store, and it's the same in every service | We needed to hand-pick exactly who's in |
| Keep serving old flags in an outage | Fail closed, or block until fresh | A flag service outage doesn't become an everything outage | A flag guarded something unsafe to run on old data |
| One agent per host | Each process connects itself | Far fewer connections, and one copy on disk | Services ran somewhere with no host agent |
| Send only what changed | Resend everything every time | Tiny updates, so changes land in seconds | The whole set was small enough to resend cheaply |
What could go wrong (5 minutes)
| What happens | What we do about it |
|---|---|
| The flag service is down | Services keep checking flags against their last copy. Changes wait until it's back |
| Someone sets a flag to 100% by mistake | Roll back to the previous version from the history. It reaches every host in seconds |
| A host's agent crashes | Processes keep their copy in memory. The agent restarts and catches up from its version |
| A new host can't reach anything | It falls back to the defaults written in code, which should be the safe choice |
| A flag name has a typo in code | It returns the code default and logs a warning, which shows up in the dashboard |
Follow-up questions (5 minutes)
How would you run experiments on top of this?
Log which bucket each merchant landed in alongside the metrics you care about, and compare them in a separate analytics system. The flag system just needs stable buckets, which hashing already gives you.
What about multiple regions?
Keep one main flag store, and put relay servers and snapshot copies in each region. If a region's cut off, it keeps its last copy, same as any other outage.
Can a flag depend on another flag?
You can allow 'only if flag X is on', but check for loops when someone saves the rule. Keep it rare, since chains of flags get hard to reason about fast.
How the 60 minutes add up
| Minutes | What you're doing |
|---|---|
| 0–5 | Agree on what you're building |
| 5–10 | Rough numbers |
| 10–20 | Draw the overall design |
| 20–30 | Checking a flag |
| 30–45 | Getting changes out, and surviving outages |
| 45–50 | Making changes safe |
| 50–55 | What could go wrong |
| 55–60 | Follow-up questions |
Key concepts used here
Further reading
We wrote this sample answer ourselves.
Practice Loop is independent and isn't affiliated with or endorsed by Stripe.