A/B Testing Frontend Design
Frontend system design for A/B experiments — assignment, flicker control, flags, metrics, and rollout tradeoffs.
- system-design
- interview
- architecture
- experiments
Scope the problem
In scope (frontend):
- Experiment assignment delivery to the client
- Preventing UI flicker (FOOC — flash of original content)
- Wiring variants into components safely
- Exposure logging and conversion events
- Interaction with SSR/CSR and feature flags
Out of scope unless asked: experiment stats engine, sequential testing math, full CDP warehouse design.
Assumptions to state: multi-variant A/B (not only on/off), logged-in + anonymous users, web-first with SSR possible.
Requirements & metrics
| Type | Examples |
|---|---|
| Functional | Assign variant, render correct UI, track exposure once |
| Non-functional | No layout flicker, low assignment latency, privacy-safe ids |
| Product | Lift on primary metric without harming Core Web Vitals |
High-level architecture
┌──────────────────────────────────────────────────────────┐
│ Edge / App server │
│ - resolve experiments for request (cookie / user id) │
│ - embed bootstrap payload in HTML │
├──────────────────────────────────────────────────────────┤
│ Client Experiment SDK │
│ - getVariation(key) - track(event) - cache assignments│
├──────────────┬───────────────────────┬───────────────────┤
│ UI variants │ Analytics bridge │ Feature flags │
│ components │ exposure + convert │ (related system) │
└──────────────┴───────────────────────┴───────────────────┘
Assignment model
type ExperimentKey = string;
type Variant = "control" | "treatment" | string;
type Assignment = {
exp: ExperimentKey;
variant: Variant;
expVersion: number;
};
type Bootstrap = {
assignments: Assignment[];
// sticky id for anonymous: from first-party cookie
subjectId: string;
};
Stickiness: same subject should see the same variant for experiment lifetime. Prefer server-set first-party cookie or logged-in user id. Don’t rely on localStorage alone for SSR-critical experiments (not available on first request).
Where assignment runs
| Placement | Pros | Cons |
|---|---|---|
| Edge / SSR | No flicker; SEO-safe HTML | Coupling; cache key fragmentation |
| Client SDK | Simple SPA | Flicker risk; delayed exposure |
| Hybrid | SSR critical exp; client rest | Complexity |
Interview default: critical above-fold experiments assigned at edge/SSR; non-critical client-side.
Flicker control
- Inline bootstrap in HTML before app JS:
window.__EXPS__ = … - CSS/class on
<html>for major layout variants (e.g.exp-nav-b) applied by tiny inline script - Block render only as last resort (hurts LCP)
- For SPA navigations, read from memory cache of assignments
Anti-pattern: fetch /assign after hydration then swap hero CTA — users and metrics both suffer.
Component integration
function CheckoutButton() {
const v = useExperiment("checkout_cta_copy");
const label =
v === "treatment" ? "Complete purchase" : "Buy now";
useExposure("checkout_cta_copy"); // once per session/assignment
return <button type="button">{label}</button>;
}
Patterns:
- Variant components —
CheckoutA/CheckoutBvia map - Config-driven — copy/color from payload (safer than shipping dead code for every test)
- Avoid sprawling
if (exp)trees without ownership
Dead code from concluded experiments becomes tech debt — require removal SLAs.
Data, caching, logging
| Layer | What | Strategy |
|---|---|---|
| Cookie / header | subject id + sticky assignments | HttpOnly where possible; careful size |
| HTML bootstrap | active assignments | Per-user; affects CDN cache keys |
| Memory | SDK cache | Session lifetime |
| Analytics | exposure, convert | Dedupe exposure; schema versioned |
Exposure: log when user actually saw the variant UI (not merely assigned). Dedupe per exp version per session.
Conversions: reuse product analytics with experiment context dimensions attached at event time.
Privacy
- Prefer first-party ids
- Honor DNT/consent modes — hold assignment or use non-personalized defaults
- Don’t put PII in experiment keys
Performance
- Assignment payload tiny (hundreds of bytes)
- Avoid extra RTT on critical path — bootstrap with document
- CDN: cache anonymous pages carefully when experiments fragment HTML (
Vary/ cache key includes exp bucket or only run client-side) - Don’t load heavy treatment bundles for users in control — split code by variant when bundles differ a lot
Feature flags vs experiments
| Feature flags | A/B experiments | |
|---|---|---|
| Goal | rollout / kill switch | measure causal lift |
| Traffic | % ramp | randomized assignment |
| Analysis | operational | statistical |
Often one platform powers both; keep semantic separation in client APIs (useFlag vs useExperiment).
Tradeoffs
- SSR assignment vs edge cache hit rate — personalization fragments cache
- Client simplicity vs flicker
- Config-driven UI vs fully separate trees
- Many simultaneous experiments — interaction effects; mutually exclusive layers
- Exposure definition — view vs interaction affects stats
Failure modes
- SDK down → fail open to control
- Partial bootstrap → don’t re-randomize client-side differently than server
- Long-running experiments with code rot
Interview close
Recap: sticky assignment, anti-flicker bootstrap, exposure vs conversion, cache implications, cleanup of winning variant. Mention sample size only at high level unless interviewer is stats-heavy.
Related on this site
- Feature Flags Client Architecture
- SSR CSR Islands Architecture
- Error Monitoring and RUM Design
- System Design Interview Framework