ESC

Type to search the knowledge base.

Feature Flags Client Architecture

Client architecture for feature flags — bootstrap, evaluation, typing, kill switches, and cache consistency.

intermediate4 min read
  • 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:

  1. Inline window.__FLAGS__ in HTML from edge/BFF
  2. SDK initializes synchronously from that object
  3. 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

  1. Pre-eval server vs client rules
  2. Flicker-free vs pure static hosting
  3. Many fine-grained flags vs complexity
  4. 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

  1. Ramp 1% → 50% → 100%
  2. Remove losing code path
  3. Delete flag and defaults
  4. Clean bootstrap payload

Flags that live >90 days without cleanup become unowned branches of doom — call that out as governance, not only tech.

Further reading