Back to System Design
System Design
hard
mid

How would you design a product like Pinterest?

Masonry-grid pinboard with infinite scroll over cursor-paginated server feed; variable-height tiles requiring a packing algorithm (CSS columns or JS positioning); virtualization for long boards; aggressive image optimization with placeholders; pin/save interactions optimistic; SSR/SSG for SEO; accessible grid + keyboard nav.

5 min read·~30 min to think through

Pinterest's distinctive feature is the masonry grid of variable-height image tiles. Most of the design follows from that constraint.

1. The masonry layout

Two approaches:

CSS columns (column-count/column-width) — simple, but items flow top-to-bottom in each column, which breaks visual reading order for many catalogs.

JS-positioned absolute tiles — compute the shortest column and place the next tile there. Required for real masonry where insertion order matters.

js
function placeTiles(tiles, columns, gap) {
  const heights = new Array(columns).fill(0);
  return tiles.map((tile) => {
    const col = heights.indexOf(Math.min(...heights));
    const pos = { left: col * (tileWidth + gap), top: heights[col] };
    heights[col] += tile.height + gap;
    return { ...tile, pos };
  });
}

CSS Grid + grid-auto-rows: 10px with each tile spanning rows proportional to its height is a modern hybrid.

2. Knowing tile height before render

You must know the image height before placing the tile — otherwise the grid jumps as images load.

  • Server returns { url, width, height } for every pin.
  • Render a fixed-size placeholder (color or blurhash) at the known aspect ratio.
  • Image swaps in when loaded; no layout shift.

3. Infinite scroll + virtualization

  • Cursor-paginated feed (offset breaks under inserts).
  • IntersectionObserver near the bottom triggers next-page fetch.
  • For very long boards, virtualize — only render tiles in/near the viewport. With masonry, virtualization needs a height index per tile so you can map scroll position to visible range.

4. Image optimization (dominant cost)

  • Responsive srcset with multiple widths; CDN resizes on demand.
  • Modern formats: AVIF/WebP with fallback.
  • Lazy-load below the fold (loading="lazy" or IntersectionObserver).
  • Blurhash / LQIP placeholder so something is visible before the full image lands.
  • Decode async to avoid main-thread blocks.

5. Interactions

  • Save / Pin → optimistic toggle, board picker modal.
  • Hover shows actions (desktop); long-press on mobile.
  • Tap opens a pin detail page (route change; deep-linkable).

6. SEO & first paint

  • SSR/SSG the home and category pages; first viewport's tiles render in HTML.
  • Hydrate for interactivity.
  • Pin detail pages are SSG with ISR.

7. Caching

  • React Query keyed by board/cursor.
  • Per-pin metadata cached aggressively (rarely changes).
  • Service worker for offline-friendly browsing.

8. Accessibility

  • Grid is a list of articles, not a literal CSS grid role — role="list" + role="listitem" on tiles.
  • Every image needs alt text (server-supplied, with a fallback).
  • Keyboard nav across tiles (Tab + arrow keys in some implementations).
  • Focus management on opening a pin overlay.

9. Performance

  • LCP is one of the above-the-fold tile images — preload it.
  • Avoid layout thrash: place tiles in a single batched pass, not per-image-load.
  • Code-split the pin detail / editor.

Interview framing

"The defining constraint is the masonry grid of variable-height image tiles. Server returns width/height with each pin so we render placeholders at the correct aspect ratio — no layout shift as images load. Tiles are placed by a shortest-column algorithm; in modern CSS, grid-auto-rows + row spans achieves the same. Cursor-paginated infinite scroll via IntersectionObserver; virtualize once boards get long. Image optimization is the dominant performance lever: srcset, AVIF/WebP, blurhash placeholders, lazy below-the-fold. SSR/SSG for SEO and LCP. Saves are optimistic. Accessibility: alt text on every image, accessible list semantics, keyboard nav."

Follow-up questions

  • Why does the server need to send width/height per pin?
  • CSS columns vs JS-positioned tiles vs CSS grid with row spans — trade-offs?
  • How do you virtualize a masonry grid?
  • What's a blurhash / LQIP and why use it?

Common mistakes

  • Placing tiles after images load — grid jumps everywhere (CLS).
  • CSS columns when insertion order matters — items go down columns.
  • Skipping virtualization on long boards.
  • No alt text / accessibility on an image-first product.

Performance considerations

  • Image bytes dominate; srcset + modern formats + lazy-load are the levers. Batched placement avoids reflow per-image-load. Virtualize long boards. Preload LCP tile.

Edge cases

  • Tile with unknown aspect ratio (no width/height) — render a fallback.
  • Window resize — recompute columns.
  • Very tall tiles dominating one column.
  • Network slow — placeholders carry the UX.

Real-world examples

  • Pinterest, Unsplash, Dribbble.
  • react-masonry-css and CSS Grid with row spans.

Senior engineer discussion

Seniors fix the layout shift problem upfront via known dimensions + placeholders, choose a layout strategy (CSS grid spans vs JS positioning) based on insertion-order needs, and treat image optimization as the primary performance work — not micro-CSS.

Related questions