Mental Model: Frontend system design is about making intentional architectural decisions — where code runs (server/client/edge), when data is fetched (build/request/client), how state flows, and what fails gracefully. Every choice has a cost: performance vs freshness, simplicity vs scalability, speed vs consistency.
| Topic | Core Concepts & Mental Model | Key Techniques & Tools | Tradeoffs & Failure Modes | Real-world Examples |
|---|---|---|---|---|
| SSG – Static Site Generation | HTML generated once at build time, served from CDN. Fastest possible TTFB — no server involved per request | fetch(url, { cache: 'force-cache' }), generateStaticParams for dynamic paths |
✅ Fastest load, CDN-friendly, cheap ❌ Stale data between deploys, long build times at scale (1000s of pages), not suitable for personalized content | Marketing pages, documentation, blogs, landing pages |
| SSR – Server-Side Rendering | HTML generated fresh on every request. Data is always current. Higher TTFB than SSG | fetch(url, { cache: 'no-store' }), getServerSideProps (Pages Router) |
✅ Fresh data, personalized, good SEO ❌ Higher TTFB, server load scales with traffic, no CDN caching for HTML | Personalized dashboards, news homepages, auth-gated pages |
| ISR – Incremental Static Regeneration | Static at build time, revalidated in background after N seconds or on-demand event. Stale-while-revalidate for pages | next: { revalidate: 60 }, revalidatePath(), revalidateTag() |
✅ CDN speed + data freshness, reduced server load ❌ Users may see stale data until revalidation completes; on-demand revalidation requires a webhook or server action trigger | E-commerce product pages, news sites, event listings |
| CSR – Client-Side Rendering | Minimal HTML shell, data fetched in browser after JS loads. Server never touches the data | 'use client' + TanStack Query / SWR in component |
✅ Rich interactivity, real-time feel, private data never touches CDN ❌ Blank screen until JS runs (bad SEO, bad slow-connection UX), hydration cost | Admin panels, post-login dashboards, user-specific feeds |
| Streaming | Server sends HTML in chunks as components resolve. Fast shell, slow parts fill in progressively — no single slow query blocks the page | <Suspense fallback={<Skeleton />}>, loading.tsx (auto-wraps segment) |
✅ Eliminates waterfall blocking, improves perceived performance ❌ Harder to reason about render order; skeleton abuse causes layout shift; requires careful Suspense boundary placement | Product pages (fast shell + slow reviews), AI chat responses |
| Partial Prerendering (PPR) (experimental) | Static shell served instantly from CDN, dynamic holes stream in — single request combines SSG speed with SSR freshness | experimental: { ppr: true }, <Suspense> marks dynamic boundaries |
✅ Best of SSG + SSR in one request ❌ Experimental — API may change; requires careful Suspense boundary design; not yet production-stable | Product pages with static layout + dynamic pricing/stock |
| Server Components | Components that run only on the server — zero JS sent to client, can access DB/secrets directly, cannot use hooks or browser APIs | Default in App Router; 'use client' opts into Client Components |
✅ Smaller JS bundle, secure, no hydration cost ❌ No interactivity, no event handlers; Server Components cannot be imported by Client Components (only passed as children) |
Blog layouts, product listings, auth-aware nav shells |
| Edge Computing | Run code at CDN edge nodes, closest to user. Web APIs only (no Node.js, no Prisma) — ultra-low latency for lightweight logic | export const runtime = 'edge', Vercel Edge Network, Cloudflare Workers |
✅ Lowest latency globally, scales automatically ❌ No Node.js APIs, no ORM, cold starts still possible, harder to debug, limited compute time | Geo-based redirects, auth token validation, A/B testing, rate limiting |
| Serverless Functions | On-demand server execution — no persistent server process. Each Route Handler / API Route is a function | Next.js Route Handlers, Vercel Functions, AWS Lambda | ✅ Auto-scaling, pay-per-use, no server management ❌ Cold starts add latency; avoid heavy initialization per request; not suited for long-running tasks (use queues instead) | Contact forms, webhooks, auth callbacks, scheduled cron jobs |
| Topic | Core Concepts & Mental Model | Key Techniques & Tools | Tradeoffs & Failure Modes | Real-world Examples |
|---|---|---|---|---|
| Flux / Unidirectional Flow | Data flows in one direction: Action → Dispatch → Store → View. No two-way binding. Predictable, auditable state changes | Redux Toolkit (createSlice, dispatch), useReducer + useContext |
✅ Predictable, debuggable, time-travel ❌ Boilerplate overhead; overkill for small apps; easy to abuse (putting server state in Redux) | Global UI state, auth session, shopping cart, multi-step wizards |
| Normalized Stores | Store relational data flat (like a DB) — entities indexed by ID, no duplication. posts[id], users[id] instead of nested objects |
Redux Toolkit createEntityAdapter, custom selectors |
✅ Single source of truth per entity, efficient updates, no sync bugs ❌ More upfront design; manual denormalization for reads; unfamiliar pattern to juniors | Posts + comments + users, products + categories, messages + senders |
| Server State vs Client State | Most "state" in apps is server data — treat it differently. Server state is async, stale, shared. Client state is synchronous, local, owned | TanStack Query / SWR (server state), Zustand / Jotai (client state) | ✅ Correct mental model eliminates "why is my data stale?" bugs ❌ Anti-pattern: storing server responses in Redux/Zustand manually instead of using a cache library | Server: posts, users, products. Client: sidebar open/closed, modal state, form step |
| URL as State | Put shareable/bookmarkable UI state in the URL — filters, pagination, tabs, search queries. Survives refresh; shareable | useSearchParams, useRouter, shallow routing |
✅ Free persistence, shareable links, back-button works ❌ URL can get cluttered; encoding complex state is awkward; syncing with component state needs care | Search filters, pagination page, selected tab, sort order |
| Topic | Core Concepts & Mental Model | Key Techniques & Tools | Tradeoffs & Failure Modes | Real-world Examples |
|---|---|---|---|---|
| REST | Resource-based URLs, stateless, HTTP verbs map to CRUD. Simple mental model, universal support | Next.js Route Handlers (GET, POST, PATCH, DELETE), Axios, fetch |
✅ Simple, universal, cacheable GETs ❌ Over-fetching (getting fields you don't need), under-fetching (multiple roundtrips for related data), no strict contract | /api/users, /api/posts/[id], /api/auth |
| GraphQL | Single endpoint, client specifies exact shape of data needed. Schema is the contract | Apollo Client, URQL, tRPC (type-safe alternative) | ✅ No over/under-fetch, great for complex relational UIs, self-documenting schema ❌ Complexity overhead for simple apps; N+1 query problem server-side (need DataLoader); caching harder than REST | GitHub-style dashboards, nested feeds (post + author + comments + likes) |
| tRPC | Type-safe RPC from client to server — no REST, no GraphQL, no codegen. TypeScript types are shared end-to-end | tRPC + Zod, Next.js adapter | ✅ End-to-end type safety, zero schema duplication, great DX ❌ Tight client-server coupling (only works within the same TypeScript monorepo); not suitable for public APIs | Internal full-stack Next.js apps, team tools, admin panels |
| Server Actions | Call server functions directly from forms or Client Components — no API route needed for mutations | 'use server' directive, useFormState / useActionState, revalidatePath |
✅ Eliminates boilerplate API routes for mutations, progressive enhancement for forms ❌ Not a replacement for public APIs; harder to reuse across services; error handling requires care | Form submissions, CRUD mutations, cart updates, profile edits |
| Pagination — Offset | Fetch page N with skip/limit. Simple to implement and understand | ?page=2&limit=20, OFFSET in SQL |
✅ Simple, supports jumping to arbitrary page ❌ Inconsistent with real-time data (inserts/deletes shift pages), performance degrades on high offsets (DB must scan all skipped rows) | E-commerce product grids, search results |
| Pagination — Cursor | Fetch next N items after a stable cursor (ID or timestamp). Consistent for real-time feeds | ?cursor=<lastId>, Prisma cursor, TanStack Query useInfiniteQuery |
✅ Consistent, performant at scale, works with real-time inserts ❌ Cannot jump to arbitrary page; cursor must be stable (use unique ID, not offset) | Twitter/Instagram feeds, chat history, infinite scroll |
| Batching | Combine multiple data requests into one round trip. Reduces waterfall and connection overhead | React's automatic RSC batching, DataLoader (server-side), Promise.all for parallel fetches |
✅ Fewer round trips, lower latency ❌ Added complexity; over-batching can delay fast responses waiting for slow ones | Profile + settings + activity feed in one SSR render |
| Optimistic Updates | Update UI instantly before server confirms. Rollback on failure | TanStack Query onMutate + onError rollback, SWR mutate |
✅ Instant-feeling UX ❌ Rollback UX can be jarring; must handle conflict when server disagrees (e.g., someone else deleted the item); not suitable for critical transactions | Like/unlike, follow/unfollow, cart add/remove, drag-and-drop reorder |
| Topic | Core Concepts & Mental Model | Key Techniques & Tools | Tradeoffs & Failure Modes | Real-world Examples |
|---|---|---|---|---|
| Short Polling | Client repeatedly asks server "any updates?" on a fixed interval | setInterval + fetch, TanStack Query refetchInterval |
✅ Simple, works everywhere ❌ Wasteful — constant requests even when nothing changed; high server load at scale; not suitable for >100 concurrent users needing low latency | Low-frequency status checks, build progress indicators |
| Long Polling | Client sends request, server holds it open until there's data to return (or timeout) | Custom server endpoint, held HTTP response | ✅ Lower latency than short polling, no persistent connection needed ❌ Server must hold many open connections; reconnect logic required; more complex than short polling | Order status updates, ticket queue notifications |
| SSE (Server-Sent Events) | Persistent one-way stream from server to client over HTTP. Built-in reconnection | EventSource API, Next.js Route Handler returning ReadableStream |
✅ Simple, HTTP-only (no upgrade), auto-reconnect, works through proxies ❌ Server → client only (no client → server on same connection); max 6 connections per domain in HTTP/1.1 | Live logs, streaming AI responses (ChatGPT-style), live sports scores |
| WebSockets | Full-duplex persistent TCP connection. Both sides can send at any time | Socket.io, native WebSocket API, Pusher, Ably (hosted) |
✅ Lowest latency bidirectional communication ❌ Custom Next.js server or external service required (Vercel serverless doesn't support persistent WebSocket connections natively); reconnect and auth logic complexity | Chat apps, collaborative editors, live trading, multiplayer games |
| Debounce & Throttle | Control rate of expensive operations triggered by user input. Debounce: wait for pause. Throttle: cap at N calls/sec | lodash.debounce, lodash.throttle, custom useDebounce hook |
❌ Common mistake: debouncing the wrong thing (debounce the API call, not the input handler); too-long debounce feels laggy; too-short defeats the purpose | Search-as-you-type (debounce), scroll/resize handlers (throttle), button spam prevention (throttle) |
| Retries with Backoff + Jitter | Retry failed requests with exponentially increasing delays + random jitter. Prevents thundering herd on recovery | TanStack Query retry + retryDelay, custom exponential backoff |
✅ Handles transient failures gracefully ❌ Without jitter, all clients retry simultaneously (thundering herd); retrying non-idempotent operations (POST) can cause duplicates — use idempotency keys | Rate-limited API calls, flaky third-party services, payment webhooks |
| Race Conditions | Multiple async operations in flight — stale response overwrites fresh one | AbortController, TanStack Query (handles internally), request ID tracking |
❌ Classic bug: type fast in search box, slow request from 3 keystrokes ago resolves after fast request from 5 keystrokes → shows wrong results | Search filters, fast navigation, tab switching with async data |
| Long-running Tasks | Never block an HTTP request with heavy work. Offload to a queue or background worker | BullMQ + Redis, Upstash QStash, Vercel Cron, Inngest, Temporal | ✅ Request returns immediately, work happens async ❌ Need observability (job status, failure visibility); user needs a way to know when it's done (SSE/polling for status) | Email sending, PDF generation, video processing, AI batch jobs, report generation |
| Offline & PWA | Cache critical assets and data for offline use. Sync changes when reconnection happens | Service Worker, Workbox, next-pwa, IndexedDB, Background Sync API |
✅ Works without connectivity, app-like install experience ❌ Cache invalidation is hard; IndexedDB API is low-level (use Dexie.js); conflict resolution on sync is complex | Field service apps, note-taking apps, delivery tracking |