Feature Flags Client Architecture
Client architecture for feature flags — bootstrap, evaluation, typing, kill switches, and cache consistency.
- system-design
- interview
- architecture
- feature-flags
Scope the problem
In scope: how the browser obtains flags, evaluates them, avoids flicker, types them, and handles fail-open/closed.
Out of scope: percentage rollout statistics (see A/B), flag admin UI backend.
Goals
- Ship dark code safely
- Instant kill switch
- Consistent evaluation per session
- Low latency; minimal extra RTT on critical path
Architecture
Build/Runtime config
+
Edge/BFF bootstrap → FlagClient (memory) → useFlag('x')
↑ │
└──── polling / WS ────┘
Payload
type FlagValue = boolean | string | number | Record<string, unknown>;
type FlagBootstrap = {
values: Record<string, FlagValue>;
// optional rules evaluated client-side
etag: string;
evaluatedAt: number;
};
Prefer pre-evaluated flags for the subject from server/edge (targeting already applied). Client-side rule engines duplicate logic and risk drift — use only for offline or UX experiments that can’t wait.
Bootstrap & anti-flicker
Same problem as experiments:
- Inline
window.__FLAGS__in HTML from edge/BFF - SDK initializes synchronously from that object
- Then refresh in background
Never: default false → paint → fetch → true (UI jump) for chrome that changes layout.
Client API
const flags = createFlagClient({
bootstrap: window.__FLAGS__,
fetch: () => api.get("/flags"),
onChange: () => rerenderSubscribers(),
});
function useFlag(key: "newCheckout" | "denseNav"): boolean {
return useSyncExternalStore(flags.subscribe, () => Boolean(flags.get(key)));
}
Typing
Generate union of flag keys from schema in CI so typos fail compile. Default values documented.
Evaluation consistency
| Requirement | Approach |
|---|---|
| Sticky per session | server assigns; cookie subject id |
| Multi-tab | same bootstrap cookie; BroadcastChannel refresh |
| SSR match CSR | server render uses same evaluation as inline bootstrap |
Mismatch SSR/CSR → hydration errors — must share values.
Refresh strategies
| Strategy | Use |
|---|---|
| Polling 60s | simple |
| ETag 304 | cheap |
| Streaming/WS | ops kill switches fast |
| Focus refetch | good enough many apps |
Kill switches: short poll interval for a small critical flags channel.
Fail behavior
function getFlag(key: string, defaultValue: boolean): boolean {
try {
const v = store[key];
return typeof v === "boolean" ? v : defaultValue;
} catch {
return defaultValue;
}
}
- Fail closed for risky features (new payment path)
- Fail open for noncritical cosmetics — product decision per flag
Caching layers
| Layer | Notes |
|---|---|
| HTML inline | strongest anti-flicker |
| memory | runtime |
| localStorage | stale offline; version carefully |
| CDN | only for anonymous global flags JSON |
Authenticated targeting → private, not public CDN.
Code splitting interaction
if (flags.get("newCheckout")) {
void import("./checkout-v2");
} else {
void import("./checkout-v1");
}
Delete losing variant after rollout completes — flags are not forever.
Governance (say out loud)
- Owner + expiry date on each flag
- Max live flags budget
- Audit log who toggled prod
Security
- Don’t put secrets in flag payloads
- Don’t trust client-evaluated admin-only flags for authorization — server enforces
Tradeoffs
- Pre-eval server vs client rules
- Flicker-free vs pure static hosting
- Many fine-grained flags vs complexity
- Shared flags service vs homemade JSON
Interview close
Bootstrap inline → typed useFlag → fail open/closed policy → refresh for kill switch → remove old code paths. Distinguish flags from A/B measurements.
Targeting inputs (server-side)
Even if the client only sees booleans, know what the evaluator used:
- user id / anonymous id
- plan tier
- percentage bucketing
- platform (web/ios)
- custom attributes (beta group)
Client should not reimplement percentage math unless offline-required.
Testing flags in dev
// force overrides for QA, disabled in prod builds
localStorage.setItem("flagOverrides", JSON.stringify({ newCheckout: true }));
Gate overrides behind internal users. Document how support reproduces a customer’s flag set (eval debugger).
Migration path off a flag
- Ramp 1% → 50% → 100%
- Remove losing code path
- Delete flag and defaults
- Clean bootstrap payload
Flags that live >90 days without cleanup become unowned branches of doom — call that out as governance, not only tech.
Related on this site
- A/B Testing Frontend Design
- SSR CSR Islands Architecture
- Client-side Routing at Scale
- System Design Interview Framework