Frontend HLD

# Pinterest โ€” Frontend High-Level Design (HLD)

Framework: Next.js App Router (React Server Components + Suspense streaming)
Structure: RADIO โ€” Requirements ยท Architecture ยท Data model ยท Interface (API) ยท Optimizations & deep dive
Companion doc: FRONTEND_DESIGN.md โ€” mini LLD with component trees, code samples, and implementation detail
System context: DESIGN.md โ€” full-stack design
Domain glossary: CONTEXT.md


# Requirements

# Functional

ID Requirement Surfaces
FR-1 Search pins by text query; results in a masonry grid /search
FR-2 Autocomplete suggestions while typing (before submit) /search
FR-3 Infinite scroll through search results /search
FR-4 Create a pin: upload image (JPEG/PNG โ‰ค 20 MB) + description /pin/create
FR-5 View a published pin after creation /pin/[id]
FR-6 Shareable search URLs (?q=) /search

# Non-functional

ID Requirement Target
NFR-1 Largest Contentful Paint (search) < 2.5 s
NFR-2 Cumulative Layout Shift (masonry) < 0.1
NFR-3 Interaction to Next Paint (scroll) < 200 ms
NFR-4 Time to First Byte (search) < 600 ms
NFR-5 Accessible search grid and suggestions WCAG-aligned patterns (see Mini LLD ยง7)
NFR-6 No global state manager Component-local state only

# Out of scope

  • Auth implementation (boundaries documented only)
  • Save-to-board UI and API
  • Pin detail related pins / masonry
  • Canvas-based pin editor (future extension โ€” see CONTEXT.md)
  • Video upload

# Constraints (locked)

  • Search: Streaming SSR on initial load, CSR for infinite scroll (ADR-0001)
  • Pin creation: CSR only โ€” no SEO value, highly interactive
  • Search indexing: eventual consistency via CDC โ€” newly published pins appear in search after backend processing (not a frontend concern)
  • Published is user-facing; internal processing status (Sharp variants) is never shown in the UI

# Architecture / High-Level Design

# Rendering strategy by route

Route Strategy Rationale
/search?q=... Streaming SSR (page 1) + CSR (scroll) SEO, shareable URLs, LCP via streamed HTML + above-fold images
/pin/create CSR Interactive upload flow; no crawl value
/pin/[id] SSR Shareable pin URLs; minimal hero + description

Frontend architecture diagram

# Core principle

Keep data fetching on the server; push interactivity to the client boundary.

Server Components own initial data loads (search results, pin detail). Client Components own browser APIs and user interaction (scroll, upload progress, suggestions debounce, masonry virtualization after hydration).

# Component layering

Search page
โ”œโ”€โ”€ SearchPage [Server]           โ€” fetches page-1 results; reads ?q= from URL
โ”œโ”€โ”€ SearchBar [Client]              โ€” input, submit, URL sync
โ”‚   โ””โ”€โ”€ SuggestionsDropdown [Client]  โ€” debounced fetch, keyboard nav
โ””โ”€โ”€ VirtualMasonryGrid [Client]     โ€” SSR initial pins; client virtualization + infinite scroll
    โ”œโ”€โ”€ PinCard [Client]            โ€” image loading, error retry
    โ””โ”€โ”€ InfiniteScrollTrigger [Client]  โ€” IntersectionObserver sentinel

Pin creation
โ””โ”€โ”€ PinCreationPage [Client]        โ€” entire page
    โ”œโ”€โ”€ FileDropzone [Client]       โ€” validate, presigned URL, XHR upload to S3
    โ”œโ”€โ”€ UploadProgress [Client]
    โ””โ”€โ”€ PinForm [Client]            โ€” description, publish

Pin detail
โ””โ”€โ”€ PinDetailPage [Server]          โ€” hero image + description + published banner

# Server vs client boundary

Concern Server Client
Initial search results โœ“
Pin detail fetch โœ“
Search query source of truth URL (?q=) โ€” read on both sides
Pagination cursor useRef in grid (never in URL)
Suggestions Debounced fetch
Masonry layout + virtualization SSR first page only Scroll, resize, infinite scroll
Image upload XHR to S3 presigned URL
Global state (Redux, Zustand) โ€” Not used

# Auth boundaries (annotated, not implemented)

Surface Access
/search, /pin/[id], GET /api/search, GET /api/suggestions Public
/pin/create, POST /api/pins, POST /api/pins/upload-url Protected (production)

# State ownership (no global store)

State Owner Storage
Search query URL ?q= โ€” shareable, SSR-readable
Pagination cursor VirtualMasonryGrid useRef โ€” opaque, client-only
Suggestions list SuggestionsDropdown useState โ€” ephemeral per keystroke
Masonry positions VirtualMasonryGrid useRef โ€” derived, not rendered
Upload progress UploadProgress useState
Pin creation id PinCreationPage useState โ€” set after presigned URL issued

# Data Model

Frontend-facing shapes โ€” source of truth remains Postgres / Elasticsearch on the backend (see DESIGN.md ยง2).

# Pin (search grid card)

type PinGridItem = {
  id: string;
  description: string;
  width: number;           // original px โ€” masonry height calc
  height: number;
  dominant_color: string;  // hex, e.g. "#a3b4c5"
  images: {
    '236w': string;        // CDN URL
    '474w': string;
  };
};

# Pin (detail page)

type PinDetail = {
  id: string;
  description: string;
  dominant_color: string;
  images: {
    '736w'?: string;
    original: string;      // fallback until variants exist
  };
};

# Search results page

type SearchResults = {
  pins: PinGridItem[];
  next_cursor: string | null;
};

# Suggestions

type Suggestions = string[];   // completion terms from ES suggester

# Upload / publish

type UploadUrlResponse = {
  pin_id: string;
  upload_url: string;
  expires_in: number;        // seconds
};

type CreatePinResponse = {
  pin_id: string;
  status: 'processing';      // internal โ€” UI treats as Published
};

# URL parameters

Param Route Purpose
q /search Search query โ€” source of truth
published /pin/[id] 1 โ†’ show published banner

Cursor is not in the URL โ€” opaque search_after token held in client useRef.


# Interface Design (API)

# Routes

Path Component type Primary data source
/search Server + Client children GET /api/search (SSR, page 1)
/pin/create Client POST /api/pins/upload-url, POST /api/pins, S3 PUT
/pin/[id] Server GET /api/pins/:id

# API surface (frontend consumer)

Endpoint When Auth (production)
GET /api/search?q=&cursor= SSR page 1 (no cursor); CSR scroll (with cursor) Public
GET /api/suggestions?q= Client, debounced on input Public
POST /api/pins/upload-url Before S3 upload Protected
POST /api/pins After upload + description Protected
GET /api/pins/:id Pin detail SSR Public
S3 presigned PUT Direct from browser (XHR) โ€”

# User flows

Search

User types โ†’ GET /api/suggestions (debounced)
  โ†’ submit โ†’ /search?q=
  โ†’ SSR GET /api/search (page 1)
  โ†’ scroll โ†’ GET /api/search?cursor=โ€ฆ โ†’ append pins

Create

Select file โ†’ POST /api/pins/upload-url โ†’ PUT S3 (XHR)
  โ†’ POST /api/pins โ†’ navigate /pin/[id]?published=1

# Optimizations & Deep Dive

# Streaming SSR (search initial load)

  1. Server reads q from searchParams (always page 1).
  2. Server fetches GET /api/search (Redis โ†’ ES).
  3. HTML shell streams immediately; <Suspense> streams skeleton then populated grid.
  4. Client hydrates with pin data in the DOM โ€” no re-fetch on hydration.

# Masonry grid

  • Layout: JS absolute positions from stored width / height; shortest-column placement; zero CLS.
  • Placeholder: dominant_color slot; image crossfades on load.
  • Virtualization: Viewport ยฑ 1 overscan; off-screen height placeholders preserve scroll.
  • Scheduling: rAF (scroll), rIC + debounced ResizeObserver ~150 ms (resize).
  • SSR: Full first page (~50 pins) on server; virtualization after hydration.

# Image loading

Context Strategy
Above-the-fold fetchPriority="high" โ€” first ~1.5 rows
Below-the-fold loading="lazy"
Grid srcset 236w + 474w
Detail hero 736w or original fallback
Load failure 2 s retry โ†’ dominant color + broken-image icon; slot preserved

# Image error handling

On onerror for a pin image:

  1. Wait 2s and retry src once โ€” catches transient mobile network drops and brief S3 hiccups silently.
  2. If retry fails: keep the pre-calculated masonry slot, display dominant color background + subtle broken-image icon overlay.
  3. Never collapse the slot โ€” collapsing requires column recalculation and causes layout shift, violating CLS.

# Caching

Two layers, complementary:

  • CDN โ€” caches the SSR HTML of the first page of results for hot queries (e.g. q=keyboards). TTL-based invalidation. Handles thundering herd on trending terms.
  • Redis โ€” caches API-layer search results (JSON) for all cursor pages and suggestion requests. Cache key: q={query}&cursor={cursor}. 60s TTL โ€” acceptable given CDC lag already introduces eventual consistency.

# Suggestions

200 ms debounce ยท AbortController per keystroke ยท lazy-loaded on first focus ยท listbox / keyboard nav (see Mini LLD ยง7).

# Pin creation UX

Published = upload + description submit succeed โ€” navigate immediately. Internal processing / Sharp / CDC never surfaced. XHR (not fetch) for S3 upload progress.

# Bundle strategy

  • App Router route splitting (automatic)
  • VirtualMasonryGrid SSR-enabled for LCP
  • SuggestionsDropdown dynamic import on focus
  • /pin/create lazy route

# Core Web Vitals

Metric Target Lever
LCP < 2.5 s Streaming SSR + fetchPriority on above-fold imgs
CLS < 0.1 Pre-sized slots from stored dimensions
INP < 200 ms rAF scroll recalc + virtualization
TTFB < 600 ms Streamed shell + CDN cache on hot queries
TBT < 300 ms rAF / rIC masonry scheduling

# Observability

Signal Indicates
LCP First-row image paint / SSR stream health
CLS Masonry slot stability
INP Scroll + virtualization performance
TTFB CDN + API latency
Upload funnel Drop-off: presigned URL โ†’ S3 โ†’ publish

# Failure modes

Scenario UX
ES down Search error boundary + retry
Slow ES Suspense skeleton until stream completes
Image CDN hiccup Silent retry โ†’ color placeholder
Scroll fetch fails Inline retry; existing pins remain
S3 upload fails Progress error; publish disabled
New pin not in search Expected (CDC lag); detail page works after publish

# Accessibility

Masonry grid:

  • Grid container: role="list"
  • Each pin card: role="listitem", aria-label="{description}"
  • Each <img>: alt="{description}"
  • Infinite scroll trigger: aria-live="polite" region announcing when new pins load
  • Keyboard navigation follows DOM (insertion) order โ€” tab order is not overridden to match visual column order

Suggestions: listbox / option dropdown ยท aria-activedescendant for keyboard highlight ยท โ†‘/โ†“ navigate, Enter submits, Escape closes.

Pin creation form: standard label/input associations, no custom a11y needed.

# Decision log

Decision Choice Rationale
Framework Next.js App Router (RSC) Streaming SSR, server data fetching
Search cursor Client useRef, not URL Opaque token; q stays shareable
Masonry SSR Full first page on server LCP; virtualization post-hydration
Upload transport XHR to S3 Upload progress events
Auth Annotated only Case study scope
Publish UX Immediate success Internal processing invisible

For calcPositions, TypeScript samples, ARIA markup, and route file tree โ€” see FRONTEND_DESIGN.md (Mini LLD).