Designing a Frontend Architecture for 1 Million Daily Active Users
February 24, 20262 min read6 views
Designing a Frontend Architecture for 1 Million Daily Active Users
CDN strategy, caching layers, rendering tradeoffs, observability, feature rollout, and incident handling. This is the frontend system design interview answer — covering every layer from edge to browser.
System Context
One million DAU means:
┌─────────────────────────────────────────────────────────────────┐
│ Traffic Profile Analysis │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1M DAU Breakdown: │
│ │
│ Peak concurrent users: ~100,000 (10% of DAU) │
│ Requests per second: ~5,000-15,000 (varies by app type) │
│ Page views per day: ~5-10M (5-10 pages/user) │
│ API calls per day: ~50-100M (10-20 API calls/page) │
│ │
│ Traffic Distribution: │
│ ├── 40% North America │
│ ├── 30% Europe │
│ ├── 20% Asia Pacific │
│ └── 10% Rest of World │
│ │
│ Device Distribution: │
│ ├── 60% Mobile │
│ ├── 35% Desktop │
│ └── 5% Tablet │
│ │
│ Peak Hours: │
│ └── 3x average during 9am-6pm in each timezone │
│ (rolling peak as timezones shift) │
│ │
└─────────────────────────────────────────────────────────────────┘
High-Level Architecture
┌─────────────────────────────────────────────────────────────────┐
│ Frontend System Architecture │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Users (1M DAU) │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ DNS (Route53/Cloudflare) │ │
│ │ GeoDNS / Latency-based │ │
│ └─────────────────────────┬───────────────────────────────┘ │
│ │ │
│ ┌────────────────────┼────────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Edge │ │ Edge │ │ Edge │ │
│ │ NA │ │ EU │ │ APAC │ │
│ │ PoPs │ │ PoPs │ │ PoPs │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │
│ │ ┌────────────┴────────────┐ │ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Edge Compute │ │ Edge Compute │ │
│ │ (Middleware) │ │ (Middleware) │ │
│ │ • Auth │ │ • Auth │ │
│ │ • A/B routing │ │ • A/B routing │ │
│ │ • Geo logic │ │ • Geo logic │ │
│ └────────┬─────────┘ └────────┬─────────┘ │
│ │ │ │
│ │ ┌─────────────────────┤ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Origin Servers │ │
│ │ (Multi-region, Auto-scaling) │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Next.js │ │ Next.js │ │ Next.js │ │ │
│ │ │ us-east │ │ eu-west │ │ ap-south │ │ │
│ │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │
│ │ │ │ │ │ │
│ │ └────────────────┼────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ Backend Services │ │ │
│ │ │ (API Gateway → Microservices → Database) │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
CDN and Edge Strategy
Multi-CDN Configuration
// cdn-config.ts
interface CDNConfig {
provider: 'cloudflare' | 'fastly' | 'vercel' | 'akamai';
regions: string[];
failoverProvider: string;
healthCheckInterval: number;
}
const cdnConfig: CDNConfig = {
provider: 'cloudflare',
regions: ['NA', 'EU', 'APAC', 'LATAM'],
failoverProvider: 'fastly',
healthCheckInterval: 10000, // 10 seconds
};
// Asset routing rules
const assetRules = {
// Immutable assets - cache forever
'/_next/static/*': {
cacheControl: 'public, max-age=31536000, immutable',
edgeTTL: 31536000,
browserTTL: 31536000,
},
// Images - long cache with revalidation
'/images/*': {
cacheControl: 'public, max-age=86400, stale-while-revalidate=604800',
edgeTTL: 604800, // 7 days at edge
browserTTL: 86400, // 1 day in browser
},
// HTML pages - short cache, SWR
'/*': {
cacheControl: 'public, max-age=0, s-maxage=60, stale-while-revalidate=86400',
edgeTTL: 60,
browserTTL: 0,
},
// API - no edge caching by default
'/api/*': {
cacheControl: 'private, no-cache',
edgeTTL: 0,
browserTTL: 0,
},
};
Edge Compute Layer
// edge-middleware.ts (Cloudflare Worker / Vercel Edge)
import { NextResponse } from 'next/server';
export const config = {
matcher: ['/((?!_next/static|favicon.ico).*)'],
};
export async function middleware(request: Request) {
const response = NextResponse.next();
const url = new URL(request.url);
// 1. Authentication at edge
const authResult = await validateAuthAtEdge(request);
if (!authResult.valid && isProtectedRoute(url.pathname)) {
return NextResponse.redirect(new URL('/login', request.url));
}
// 2. Feature flags
const flags = await getFeatureFlags(request);
response.headers.set('x-feature-flags', JSON.stringify(flags));
// 3. A/B test bucketing
const experiments = assignExperiments(request);
response.headers.set('x-experiments', JSON.stringify(experiments));
// 4. Geographic personalization
const geo = {
country: request.geo?.country ?? 'US',
region: request.geo?.region ?? '',
city: request.geo?.city ?? '',
};
response.headers.set('x-geo', JSON.stringify(geo));
// 5. Bot detection
if (isBot(request)) {
response.headers.set('x-bot', 'true');
// Serve simplified version or block
}
// 6. Rate limiting
const rateLimited = await checkRateLimit(request);
if (rateLimited) {
return new NextResponse('Too Many Requests', { status: 429 });
}
return response;
}
async function validateAuthAtEdge(request: Request): Promise<{ valid: boolean; user?: User }> {
const token = request.cookies.get('session')?.value;
if (!token) return { valid: false };
// Validate JWT at edge (stateless)
try {
const payload = await verifyJWT(token);
return { valid: true, user: payload };
} catch {
return { valid: false };
}
}
function assignExperiments(request: Request): Record<string, string> {
const userId = request.cookies.get('userId')?.value ?? crypto.randomUUID();
// Deterministic assignment based on user ID
return {
'checkout-v2': hashBucket(userId, 'checkout-v2') < 0.5 ? 'control' : 'variant',
'new-nav': hashBucket(userId, 'new-nav') < 0.2 ? 'enabled' : 'disabled',
};
}
function hashBucket(userId: string, experimentId: string): number {
const hash = simpleHash(`${userId}:${experimentId}`);
return (hash % 1000) / 1000; // 0-1 range
}
CDN Cache Invalidation
// cache-invalidation.ts
interface InvalidationTarget {
paths?: string[];
tags?: string[];
prefixes?: string[];
}
class CDNInvalidator {
private providers: Map<string, CDNProvider> = new Map();
constructor() {
this.providers.set('cloudflare', new CloudflareProvider());
this.providers.set('fastly', new FastlyProvider());
this.providers.set('vercel', new VercelProvider());
}
async invalidate(target: InvalidationTarget): Promise<void> {
const invalidations = Array.from(this.providers.values()).map((provider) =>
provider.purge(target)
);
await Promise.all(invalidations);
// Log for audit
await this.logInvalidation(target);
}
async invalidateProduct(productId: string): Promise<void> {
await this.invalidate({
tags: [`product-${productId}`, 'product-list'],
paths: [`/products/${productId}`, '/products'],
});
}
async invalidateGlobal(): Promise<void> {
// Nuclear option - use sparingly
await this.invalidate({
prefixes: ['/'],
});
}
}
class CloudflareProvider implements CDNProvider {
async purge(target: InvalidationTarget): Promise<void> {
const ZONE_ID = process.env.CF_ZONE_ID!;
const API_TOKEN = process.env.CF_API_TOKEN!;
if (target.tags) {
await fetch(
`https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/purge_cache`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${API_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ tags: target.tags }),
}
);
}
if (target.paths) {
await fetch(
`https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/purge_cache`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${API_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
files: target.paths.map((p) => `https://example.com${p}`),
}),
}
);
}
}
}
Caching Layers
┌─────────────────────────────────────────────────────────────────┐
│ Multi-Layer Cache Strategy │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Layer 1: Browser Cache │
│ ├── HTTP Cache (Cache-Control headers) │
│ ├── Service Worker Cache (offline + prefetch) │
│ ├── React Query / SWR Cache (API responses) │
│ └── Memory Cache (component state) │
│ │
│ Layer 2: Edge Cache (CDN) │
│ ├── Static assets (immutable, 1 year) │
│ ├── HTML pages (short TTL + SWR) │
│ ├── API responses (selective caching) │
│ └── ISR pages (revalidate on demand) │
│ │
│ Layer 3: Application Cache │
│ ├── Next.js Data Cache (fetch memoization) │
│ ├── Next.js Full Route Cache (RSC payloads) │
│ ├── Redis (session, computed data) │
│ └── In-memory LRU (hot data) │
│ │
│ Layer 4: Database Cache │
│ ├── Query result cache │
│ ├── Prepared statement cache │
│ └── Connection pool │
│ │
└─────────────────────────────────────────────────────────────────┘
Cache Implementation
// cache-strategy.ts
interface CacheConfig {
key: string;
ttl: number;
staleWhileRevalidate?: number;
tags?: string[];
}
class MultiLayerCache {
private memory: LRUCache<string, unknown>;
private redis: RedisClient;
constructor() {
this.memory = new LRUCache({ max: 1000, ttl: 60000 });
this.redis = createRedisClient();
}
async get<T>(config: CacheConfig): Promise<T | null> {
// Try memory first
const memoryHit = this.memory.get(config.key);
if (memoryHit) {
metrics.increment('cache.memory.hit');
return memoryHit as T;
}
// Try Redis
const redisHit = await this.redis.get(config.key);
if (redisHit) {
metrics.increment('cache.redis.hit');
// Populate memory cache
this.memory.set(config.key, JSON.parse(redisHit));
return JSON.parse(redisHit) as T;
}
metrics.increment('cache.miss');
return null;
}
async set<T>(config: CacheConfig, value: T): Promise<void> {
const serialized = JSON.stringify(value);
// Set in both layers
this.memory.set(config.key, value);
await this.redis.setex(config.key, config.ttl, serialized);
// Store tags for invalidation
if (config.tags) {
for (const tag of config.tags) {
await this.redis.sadd(`tag:${tag}`, config.key);
}
}
}
async invalidateByTag(tag: string): Promise<void> {
const keys = await this.redis.smembers(`tag:${tag}`);
// Delete from both layers
for (const key of keys) {
this.memory.delete(key);
await this.redis.del(key);
}
await this.redis.del(`tag:${tag}`);
}
}
// React Query configuration for browser
const queryClientConfig = {
defaultOptions: {
queries: {
staleTime: 30 * 1000, // 30 seconds
gcTime: 5 * 60 * 1000, // 5 minutes
refetchOnWindowFocus: true,
retry: 3,
retryDelay: (attemptIndex: number) =>
Math.min(1000 * 2 ** attemptIndex, 30000),
},
},
};
Rendering Strategy
Decision Framework
┌─────────────────────────────────────────────────────────────────┐
│ Rendering Strategy Matrix │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Page Type │ Strategy │ Cache │ Personalized │
│ ───────────────────┼─────────────┼──────────┼──────────────── │
│ Landing pages │ SSG │ CDN/long │ No │
│ Marketing │ SSG │ CDN/long │ No │
│ Product listing │ ISR (60s) │ CDN/med │ Partial │
│ Product detail │ ISR (60s) │ CDN/med │ Partial │
│ Search results │ SSR │ Short │ Yes (query) │
│ User dashboard │ SSR │ None │ Yes │
│ Settings │ SSR │ None │ Yes │
│ Checkout │ CSR │ None │ Yes │
│ Real-time feeds │ CSR + WS │ None │ Yes │
│ │
│ Legend: │
│ SSG = Static Site Generation (build time) │
│ ISR = Incremental Static Regeneration │
│ SSR = Server-Side Rendering (request time) │
│ CSR = Client-Side Rendering │
│ │
└─────────────────────────────────────────────────────────────────┘
Implementation
// app/products/page.tsx - ISR with on-demand revalidation
export const revalidate = 60; // Revalidate every 60 seconds
export default async function ProductsPage() {
const products = await getProducts();
return (
<ProductGrid products={products} />
);
}
// app/products/[id]/page.tsx - ISR with dynamic paths
export async function generateStaticParams() {
// Pre-generate top 1000 products
const topProducts = await db.products.findMany({
orderBy: { views: 'desc' },
take: 1000,
select: { id: true },
});
return topProducts.map((p) => ({ id: p.id }));
}
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id);
if (!product) {
notFound();
}
return (
<>
{/* Static shell - cached */}
<ProductHeader product={product} />
<ProductImages images={product.images} />
<ProductDescription description={product.description} />
{/* Dynamic islands - client rendered */}
<Suspense fallback={<PriceSkeleton />}>
<ProductPrice productId={product.id} />
</Suspense>
<Suspense fallback={<InventorySkeleton />}>
<InventoryStatus productId={product.id} />
</Suspense>
<Suspense fallback={<ReviewsSkeleton />}>
<ProductReviews productId={product.id} />
</Suspense>
</>
);
}
// app/dashboard/page.tsx - SSR (no caching)
export const dynamic = 'force-dynamic';
export default async function DashboardPage() {
const user = await getCurrentUser();
const dashboardData = await getDashboardData(user.id);
return <Dashboard data={dashboardData} />;
}
Observability
Metrics Collection
// observability/metrics.ts
interface Metric {
name: string;
type: 'counter' | 'histogram' | 'gauge';
value: number;
labels: Record<string, string>;
timestamp: number;
}
class FrontendMetrics {
private buffer: Metric[] = [];
private flushInterval = 10000; // 10 seconds
constructor() {
if (typeof window !== 'undefined') {
setInterval(() => this.flush(), this.flushInterval);
window.addEventListener('beforeunload', () => this.flush());
}
}
// Core Web Vitals
collectWebVitals(): void {
if (typeof window === 'undefined') return;
// LCP
new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1] as PerformanceEntry;
this.histogram('web_vitals_lcp', lastEntry.startTime, {
page: window.location.pathname,
});
}).observe({ entryTypes: ['largest-contentful-paint'] });
// FID / INP
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
this.histogram('web_vitals_inp', entry.duration, {
page: window.location.pathname,
type: entry.name,
});
}
}).observe({ entryTypes: ['event'] });
// CLS
let clsValue = 0;
new PerformanceObserver((list) => {
for (const entry of list.getEntries() as PerformanceEntry[]) {
if (!(entry as any).hadRecentInput) {
clsValue += (entry as any).value;
}
}
this.gauge('web_vitals_cls', clsValue, {
page: window.location.pathname,
});
}).observe({ entryTypes: ['layout-shift'] });
// TTFB
const nav = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
if (nav) {
this.histogram('web_vitals_ttfb', nav.responseStart - nav.requestStart, {
page: window.location.pathname,
});
}
}
// Custom metrics
counter(name: string, labels: Record<string, string> = {}): void {
this.buffer.push({
name,
type: 'counter',
value: 1,
labels,
timestamp: Date.now(),
});
}
histogram(name: string, value: number, labels: Record<string, string> = {}): void {
this.buffer.push({
name,
type: 'histogram',
value,
labels,
timestamp: Date.now(),
});
}
gauge(name: string, value: number, labels: Record<string, string> = {}): void {
this.buffer.push({
name,
type: 'gauge',
value,
labels,
timestamp: Date.now(),
});
}
private async flush(): Promise<void> {
if (this.buffer.length === 0) return;
const metrics = [...this.buffer];
this.buffer = [];
try {
await fetch('/api/metrics', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ metrics }),
keepalive: true,
});
} catch {
// Re-add to buffer on failure
this.buffer.push(...metrics);
}
}
}
export const metrics = new FrontendMetrics();
Distributed Tracing
// observability/tracing.ts
interface Span {
traceId: string;
spanId: string;
parentSpanId?: string;
operationName: string;
startTime: number;
endTime?: number;
tags: Record<string, string>;
logs: Array<{ timestamp: number; message: string }>;
}
class FrontendTracer {
private currentTraceId: string | null = null;
// Start trace from server-provided ID or generate new
initTrace(traceId?: string): string {
this.currentTraceId = traceId ?? this.generateId();
return this.currentTraceId;
}
startSpan(operationName: string, parentSpanId?: string): Span {
return {
traceId: this.currentTraceId ?? this.generateId(),
spanId: this.generateId(),
parentSpanId,
operationName,
startTime: performance.now(),
tags: {},
logs: [],
};
}
endSpan(span: Span): void {
span.endTime = performance.now();
this.reportSpan(span);
}
// Wrap fetch with tracing
tracedFetch = async (url: string, options: RequestInit = {}): Promise<Response> => {
const span = this.startSpan('fetch');
span.tags['http.url'] = url;
span.tags['http.method'] = options.method ?? 'GET';
// Add trace headers
const headers = new Headers(options.headers);
headers.set('x-trace-id', span.traceId);
headers.set('x-span-id', span.spanId);
try {
const response = await fetch(url, { ...options, headers });
span.tags['http.status_code'] = String(response.status);
return response;
} catch (error) {
span.tags['error'] = 'true';
span.logs.push({
timestamp: performance.now(),
message: String(error),
});
throw error;
} finally {
this.endSpan(span);
}
};
private generateId(): string {
return crypto.randomUUID().replace(/-/g, '').slice(0, 16);
}
private async reportSpan(span: Span): Promise<void> {
await fetch('/api/traces', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(span),
keepalive: true,
});
}
}
export const tracer = new FrontendTracer();
Error Tracking
// observability/errors.ts
interface ErrorReport {
message: string;
stack?: string;
type: 'error' | 'unhandledrejection' | 'react-error-boundary';
url: string;
userAgent: string;
timestamp: number;
userId?: string;
sessionId: string;
context: Record<string, unknown>;
breadcrumbs: Breadcrumb[];
}
interface Breadcrumb {
type: 'click' | 'navigation' | 'console' | 'fetch' | 'xhr';
message: string;
timestamp: number;
data?: Record<string, unknown>;
}
class ErrorTracker {
private breadcrumbs: Breadcrumb[] = [];
private maxBreadcrumbs = 50;
private sessionId = crypto.randomUUID();
init(): void {
if (typeof window === 'undefined') return;
// Global error handler
window.addEventListener('error', (event) => {
this.captureError(event.error ?? new Error(event.message), {
type: 'error',
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
});
});
// Unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
this.captureError(event.reason, { type: 'unhandledrejection' });
});
// Track clicks for breadcrumbs
document.addEventListener('click', (event) => {
const target = event.target as HTMLElement;
this.addBreadcrumb({
type: 'click',
message: `Clicked ${target.tagName.toLowerCase()}${target.id ? `#${target.id}` : ''}`,
timestamp: Date.now(),
data: {
selector: this.getSelector(target),
text: target.textContent?.slice(0, 100),
},
});
});
// Track navigation
window.addEventListener('popstate', () => {
this.addBreadcrumb({
type: 'navigation',
message: `Navigated to ${window.location.pathname}`,
timestamp: Date.now(),
});
});
// Intercept console.error
const originalError = console.error;
console.error = (...args) => {
this.addBreadcrumb({
type: 'console',
message: args.map(String).join(' '),
timestamp: Date.now(),
});
originalError.apply(console, args);
};
}
captureError(error: Error, context: Record<string, unknown> = {}): void {
const report: ErrorReport = {
message: error.message,
stack: error.stack,
type: (context.type as ErrorReport['type']) ?? 'error',
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: Date.now(),
sessionId: this.sessionId,
context,
breadcrumbs: [...this.breadcrumbs],
};
this.sendReport(report);
}
private addBreadcrumb(breadcrumb: Breadcrumb): void {
this.breadcrumbs.push(breadcrumb);
if (this.breadcrumbs.length > this.maxBreadcrumbs) {
this.breadcrumbs.shift();
}
}
private async sendReport(report: ErrorReport): Promise<void> {
try {
await fetch('/api/errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(report),
keepalive: true,
});
} catch {
// Fallback to beacon
navigator.sendBeacon('/api/errors', JSON.stringify(report));
}
}
private getSelector(element: HTMLElement): string {
const parts: string[] = [];
let current: HTMLElement | null = element;
while (current && current !== document.body) {
let selector = current.tagName.toLowerCase();
if (current.id) {
selector += `#${current.id}`;
parts.unshift(selector);
break;
}
if (current.className) {
selector += `.${current.className.split(' ').join('.')}`;
}
parts.unshift(selector);
current = current.parentElement;
}
return parts.join(' > ');
}
}
export const errorTracker = new ErrorTracker();
Feature Rollout
Feature Flag System
// features/flags.ts
interface FeatureFlag {
name: string;
enabled: boolean;
rolloutPercentage: number;
targetRules: TargetRule[];
variants?: Record<string, unknown>;
}
interface TargetRule {
attribute: string;
operator: 'equals' | 'contains' | 'startsWith' | 'in';
value: string | string[];
}
interface UserContext {
userId: string;
email?: string;
country?: string;
userAgent?: string;
groups?: string[];
}
class FeatureFlagService {
private flags: Map<string, FeatureFlag> = new Map();
private userContext: UserContext | null = null;
async init(): Promise<void> {
// Fetch flags from server
const response = await fetch('/api/feature-flags');
const flags = await response.json();
for (const flag of flags) {
this.flags.set(flag.name, flag);
}
}
setUserContext(context: UserContext): void {
this.userContext = context;
}
isEnabled(flagName: string): boolean {
const flag = this.flags.get(flagName);
if (!flag) return false;
if (!flag.enabled) return false;
// Check targeting rules
if (flag.targetRules.length > 0 && this.userContext) {
const matchesRules = flag.targetRules.every((rule) =>
this.evaluateRule(rule, this.userContext!)
);
if (!matchesRules) return false;
}
// Check rollout percentage
if (flag.rolloutPercentage < 100) {
if (!this.userContext?.userId) return false;
const bucket = this.hashToBucket(this.userContext.userId, flagName);
return bucket < flag.rolloutPercentage;
}
return true;
}
getVariant<T>(flagName: string): T | null {
const flag = this.flags.get(flagName);
if (!flag || !this.isEnabled(flagName)) return null;
return (flag.variants as T) ?? null;
}
private evaluateRule(rule: TargetRule, context: UserContext): boolean {
const value = context[rule.attribute as keyof UserContext];
if (value === undefined) return false;
switch (rule.operator) {
case 'equals':
return value === rule.value;
case 'contains':
return String(value).includes(rule.value as string);
case 'startsWith':
return String(value).startsWith(rule.value as string);
case 'in':
return (rule.value as string[]).includes(String(value));
default:
return false;
}
}
private hashToBucket(userId: string, flagName: string): number {
const hash = this.simpleHash(`${userId}:${flagName}`);
return hash % 100;
}
private simpleHash(str: string): number {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) - hash + str.charCodeAt(i)) | 0;
}
return Math.abs(hash);
}
}
export const featureFlags = new FeatureFlagService();
// React hook
export function useFeatureFlag(flagName: string): boolean {
const [enabled, setEnabled] = useState(false);
useEffect(() => {
setEnabled(featureFlags.isEnabled(flagName));
}, [flagName]);
return enabled;
}
Gradual Rollout Strategy
// features/rollout.ts
interface RolloutPlan {
flagName: string;
stages: RolloutStage[];
currentStage: number;
rollbackTriggers: RollbackTrigger[];
}
interface RolloutStage {
percentage: number;
duration: number; // hours to wait before next stage
criteria: {
maxErrorRate: number;
maxP95Latency: number;
minSampleSize: number;
};
}
interface RollbackTrigger {
metric: string;
threshold: number;
window: number; // seconds
}
class RolloutManager {
async startRollout(plan: RolloutPlan): Promise<void> {
const stage = plan.stages[plan.currentStage];
// Update flag rollout percentage
await this.updateFlagPercentage(plan.flagName, stage.percentage);
// Schedule health check
setTimeout(
() => this.evaluateStage(plan),
stage.duration * 60 * 60 * 1000
);
// Start monitoring
this.monitorRollout(plan);
}
private async evaluateStage(plan: RolloutPlan): Promise<void> {
const stage = plan.stages[plan.currentStage];
const metrics = await this.getStageMetrics(plan.flagName);
// Check criteria
const passed =
metrics.errorRate <= stage.criteria.maxErrorRate &&
metrics.p95Latency <= stage.criteria.maxP95Latency &&
metrics.sampleSize >= stage.criteria.minSampleSize;
if (passed && plan.currentStage < plan.stages.length - 1) {
// Advance to next stage
plan.currentStage++;
await this.startRollout(plan);
} else if (!passed) {
// Rollback
await this.rollback(plan);
}
// else: rollout complete at 100%
}
private monitorRollout(plan: RolloutPlan): void {
const interval = setInterval(async () => {
for (const trigger of plan.rollbackTriggers) {
const value = await this.getMetricValue(
trigger.metric,
plan.flagName,
trigger.window
);
if (value > trigger.threshold) {
await this.rollback(plan);
clearInterval(interval);
return;
}
}
}, 10000); // Check every 10 seconds
}
private async rollback(plan: RolloutPlan): Promise<void> {
await this.updateFlagPercentage(plan.flagName, 0);
await this.notifyTeam(`Rollback triggered for ${plan.flagName}`);
}
}
Incident Handling
Circuit Breaker
// resilience/circuit-breaker.ts
type CircuitState = 'closed' | 'open' | 'half-open';
interface CircuitBreakerConfig {
failureThreshold: number;
resetTimeout: number;
halfOpenRequests: number;
}
class CircuitBreaker {
private state: CircuitState = 'closed';
private failures = 0;
private lastFailure: number | null = null;
private halfOpenSuccesses = 0;
constructor(
private name: string,
private config: CircuitBreakerConfig
) {}
async execute<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === 'open') {
if (this.shouldAttemptReset()) {
this.state = 'half-open';
this.halfOpenSuccesses = 0;
} else {
throw new CircuitOpenError(this.name);
}
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess(): void {
if (this.state === 'half-open') {
this.halfOpenSuccesses++;
if (this.halfOpenSuccesses >= this.config.halfOpenRequests) {
this.state = 'closed';
this.failures = 0;
}
} else {
this.failures = 0;
}
}
private onFailure(): void {
this.failures++;
this.lastFailure = Date.now();
if (this.failures >= this.config.failureThreshold) {
this.state = 'open';
metrics.counter('circuit_breaker.opened', { name: this.name });
}
}
private shouldAttemptReset(): boolean {
if (!this.lastFailure) return true;
return Date.now() - this.lastFailure >= this.config.resetTimeout;
}
getState(): CircuitState {
return this.state;
}
}
// Usage
const apiCircuit = new CircuitBreaker('api', {
failureThreshold: 5,
resetTimeout: 30000,
halfOpenRequests: 3,
});
async function fetchWithCircuitBreaker<T>(url: string): Promise<T> {
return apiCircuit.execute(() =>
fetch(url).then((r) => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
})
);
}
Graceful Degradation
// resilience/degradation.ts
interface DegradationConfig {
feature: string;
fallback: () => React.ReactNode;
timeout: number;
}
class DegradationManager {
private degradedFeatures: Set<string> = new Set();
degrade(feature: string): void {
this.degradedFeatures.add(feature);
metrics.counter('feature.degraded', { feature });
}
restore(feature: string): void {
this.degradedFeatures.delete(feature);
metrics.counter('feature.restored', { feature });
}
isDegraded(feature: string): boolean {
return this.degradedFeatures.has(feature);
}
}
export const degradation = new DegradationManager();
// React component wrapper
function DegradableFeature({
name,
fallback,
children,
}: {
name: string;
fallback: React.ReactNode;
children: React.ReactNode;
}) {
const [degraded, setDegraded] = useState(degradation.isDegraded(name));
useEffect(() => {
// Listen for degradation events
const handler = (event: CustomEvent) => {
if (event.detail.feature === name) {
setDegraded(event.detail.degraded);
}
};
window.addEventListener('degradation-change', handler as EventListener);
return () => window.removeEventListener('degradation-change', handler as EventListener);
}, [name]);
if (degraded) {
return <>{fallback}</>;
}
return <>{children}</>;
}
// Usage
function ProductRecommendations({ productId }: { productId: string }) {
return (
<DegradableFeature
name="recommendations"
fallback={<StaticRecommendations />}
>
<DynamicRecommendations productId={productId} />
</DegradableFeature>
);
}
Incident Response Automation
// incident/response.ts
interface Incident {
id: string;
severity: 'critical' | 'high' | 'medium' | 'low';
type: 'performance' | 'availability' | 'error-rate';
metric: string;
threshold: number;
currentValue: number;
startTime: number;
status: 'active' | 'mitigated' | 'resolved';
}
class IncidentResponder {
private activeIncidents: Map<string, Incident> = new Map();
async handleAlert(alert: AlertPayload): Promise<void> {
const incident = this.createIncident(alert);
this.activeIncidents.set(incident.id, incident);
// Auto-mitigation based on incident type
await this.autoMitigate(incident);
// Notify team
await this.notifyTeam(incident);
// Update status page
await this.updateStatusPage(incident);
}
private async autoMitigate(incident: Incident): Promise<void> {
switch (incident.type) {
case 'performance':
// Reduce traffic to affected region
await this.reduceRegionTraffic(incident);
// Enable aggressive caching
await this.enableEmergencyCache();
break;
case 'availability':
// Failover to backup region
await this.triggerFailover(incident);
// Enable maintenance mode if needed
break;
case 'error-rate':
// Roll back recent deployments
await this.checkAndRollback(incident);
// Enable circuit breakers
await this.enableCircuitBreakers();
break;
}
incident.status = 'mitigated';
}
private async reduceRegionTraffic(incident: Incident): Promise<void> {
// Reduce traffic to 50%
await fetch('/api/traffic-control', {
method: 'POST',
body: JSON.stringify({
action: 'reduce',
percentage: 50,
duration: 30 * 60, // 30 minutes
}),
});
}
private async triggerFailover(incident: Incident): Promise<void> {
await fetch('/api/failover', {
method: 'POST',
body: JSON.stringify({
fromRegion: this.getAffectedRegion(incident),
toRegion: this.getBackupRegion(incident),
}),
});
}
private async checkAndRollback(incident: Incident): Promise<void> {
const recentDeploys = await this.getRecentDeployments();
for (const deploy of recentDeploys) {
const correlation = await this.checkDeployCorrelation(deploy, incident);
if (correlation > 0.8) {
await this.rollbackDeployment(deploy.id);
break;
}
}
}
}
Summary
Frontend architecture for 1M DAU requires:
| Component | Strategy | Key Metrics |
|---|---|---|
| CDN | Multi-PoP, edge compute, intelligent caching | Cache hit rate >95% |
| Caching | 4-layer (browser, CDN, app, DB) | TTFB <200ms |
| Rendering | ISR for catalog, SSR for personalized, CSR for real-time | LCP <2.5s |
| Observability | Web Vitals, distributed tracing, error tracking | P95 tracking |
| Feature Rollout | Gradual rollout with automatic rollback | Error rate <0.1% |
| Incident Handling | Circuit breakers, graceful degradation, auto-mitigation | MTTR <15min |
The architecture must handle:
- 5,000-15,000 RPS at peak
- Global distribution across 4+ regions
- Zero-downtime deployments
- Automatic incident response
- Real-time feature control
Scale horizontally at every layer, fail gracefully at every boundary, observe everything.
What did you think?