cf.completefrontend
My progress
SYSTEM DESIGN · #85

Design Stack Overflow

The web client of a programming Q&A site: question pages that search engines send millions of signed-out readers to, plus asking, answering, voting, editing, and moderation for signed-in members. The hard parts are serving one cached page to every anonymous reader while still showing each member their own votes and unlocked actions, an ask flow that catches duplicates before they are posted, and rules that depend on the viewer's reputation.

The question

Design the web client for Stack Overflow. A question page shows one question, its answers, short comments under each post, vote counts, tags, and a box to write your own answer. Signed-in members ask questions, answer them, vote, edit posts, and flag or close bad ones. What a member may do depends on their reputation.

Most visitors arrive from a search engine, are not signed in, and read one page before leaving. Walk me through how you render and cache the question page, how the page stays correct for a signed-in member, how voting and editing work on the client, and how the ask flow helps people avoid posting duplicates.

Real-life examples: Stack Overflow, Stack Exchange sites, GitHub Discussions, Discourse, Quora

Requirements: the questions that make this Stack Overflow

A Q&A site looks like a forum, and parts of it are solved in other guides. The Reddit guide covers three-state voting with optimistic updates, score fuzzing, and ranked feeds. The Wikipedia guide covers serving cached HTML to anonymous readers, revision history, and edit conflicts.

The Markdown editor guide covers the editor with live preview and sanitizing, and the Code review diff guide covers comparing two revisions. Tags, search, comments, and the inbox have their own guides too. This guide links to all of those and spends its time on what only Stack Overflow has.

What only Stack Overflow has is a page with a job. A question page is the money page: it is the page Google sends people to, the page ads run on, and the page that turns a reader into someone who answers. Everything else on the site (the ask form, the review queues, the user profiles) exists to make more good question pages. So the first five minutes go to scope, and most of the questions are about that one page and who looks at it.

QuestionAnswer we assumeWhat it changes
Who reads the question page?Mostly signed-out visitors from a search engine, on desktop and mobile. Signed-in members are a small share of views but do all the writing.The page must be complete, fast HTML that can be cached and shared by every anonymous reader. Personal parts load separately.
How big is it?Tens of millions of questions, each a permanent URL. Hundreds of millions of page views a month (an estimate; the public numbers have changed over the years).We cannot prerender everything at build time. Pages render on request and are cached at the edge (a CDN: servers near the reader that keep copies of responses).
What can a member do?Ask, answer, comment, vote up and down, accept an answer on their own question, edit, flag, and vote to close.Many small write actions on one page, each with its own rules.
Do the rules depend on the user?Yes. Each action unlocks at a reputation level, and some cost reputation.The client must know the viewer's privileges, show locked actions with a reason, and still let the server decide.
How do answers sort?By score by default, with sorts for recent activity and date. Long answer lists are paged.Sort lives in the URL. Ordering must not jump while someone votes.
Live updates?A notice when a new answer or edit arrives, plus inbox and reputation counters in the top bar.One socket per tab for a few small event types, not a live feed.
Search engines?They must see the full question, all answers on the first page, and structured data.Server rendering, canonical URLs, and rules for what not to index.

Functional requirements

  • Show a question page at /questions/{id}/{slug}: title, body, tags, author card, score, answers with their own scores and author cards, comments under each post, related and linked questions, and an answer box.
  • Let members vote up or down on questions and answers, and let the asker accept one answer.
  • Let members ask a question with a title, a markdown body, and 1 to 5 tags, warn them about similar existing questions while they type, and keep a draft if they leave.
  • Let members post answers and comments, and edit posts (directly at high reputation, as a suggested edit below it).
  • Let members flag posts and vote to close them, including as a duplicate of another question.
  • Notify members of new answers on the page they are reading, and of replies and reputation changes anywhere on the site.

Non-functional requirements

  • Fast first view for anonymous readers. Target a Largest Contentful Paint (the time until the biggest text block or image is shown) under 2.5 seconds on a mid-range phone, and a time to first byte under 200 ms from the edge cache.
  • Correct for the viewer. A signed-in member must never see another person's vote state or buttons. A cached page must never contain anything personal.
  • Indexable. The question, answers, and code are in the server HTML, with one canonical URL per question.
  • Stable while you read. Votes, new answers, and late-loading parts must not move the text under the reader's eyes (a low Cumulative Layout Shift, under 0.1).
  • Accessible. Votes, accept marks, and code blocks work with a keyboard and a screen reader.
  • Safe. User markdown becomes HTML only through a sanitizer, because every post is shown to millions of people.

Out of scope

We leave out the search page (see the Search results page guide), chat, jobs, Stack Overflow for Teams, the full moderator dashboard, and the backend ranking and reputation jobs. We treat the reputation numbers as data the server sends us, not as logic we write.

Check your understanding · Exercise 1

Which answers from the interviewer would change the design of the question page the most? Select all that apply.

What makes Stack Overflow hard

Four traits of this product drive every decision in the rest of the guide. Keep them in mind, because each later chapter answers one of them.

  1. One page, two audiences. The same URL is read by an anonymous visitor who wants the answer and leaves in a minute, and by a member who wants to vote, comment, and edit. The visitor needs speed and nothing else. The member needs personal state on every post. If we render personal state into the HTML, the page stops being cacheable. If we leave it out, the member sees wrong buttons until it arrives.
  2. Rules that depend on who you are. The same down arrow is a working button for one member, a locked button with an explanation for another, and a sign-in prompt for a visitor. These rules come from the reputation system and change over time, so the client must not hard-code them as the final word.
  3. Read-mostly, but writes are visible to everyone. A single question may get thousands of views for each vote, and far fewer edits. Yet when someone edits a post, every cached copy of the page is now stale. We need a cache that serves almost everything and an invalidation story for the rare write.
  4. The content is code. Posts are markdown full of code blocks in hundreds of languages. Code must never wrap, must be highlighted, must be copyable, and must be safe: a post is user input shown to millions of strangers.

Compare this with Reddit. Reddit's hard part is a fast-moving ranked feed, while Stack Overflow's question page barely moves: a good question from 2012 may still be read every day a decade later.

That is why Stack Overflow can lean so hard on caching, and why its engineers have written about running one of the busiest sites on the web with a small number of servers. We come back to that in the next chapter.

Think first

Before reading on: if 95 out of 100 views of a question page are anonymous, what is the cost of rendering every view from scratch on the server, and what is the cost of caching the whole page including the member's buttons?

The question page: one cached page, many viewers

Start with the page that matters most. A reader types an error message into Google, clicks a result, and lands on /questions/11227809/why-is-processing-a-sorted-array-faster-than-processing-an-unsorted-array. They have never visited before, they have no cookies, and they want the accepted fix in under three seconds. Nothing on that page is personal to them.

Now a member opens the same URL. They see the same question and answers, but also: which arrows they already clicked, whether they may downvote, an edit link that applies at once or goes to review, a close link if they have 3,000 reputation, their inbox count, and their avatar in the top bar. That is maybe 1 KB of personal data on top of 100 KB of shared HTML.

The naive approaches, and where they break

Naive 1: a single-page app. Ship an empty shell, then fetch the question as JSON and render it in the browser. The crawler and the reader both wait for JavaScript, the main content shows late, and the Largest Contentful Paint on a mid-range phone easily passes 3 seconds. For a page whose whole value is its text, this is the wrong default.

Naive 2: server render everything, per request, with personal state baked in. The HTML is complete and indexable, but it cannot be shared: a response with one member's vote arrows must be marked Cache-Control: private. So every anonymous view hits the origin. At millions of question views a day, that is a lot of identical work, and every traffic spike from a viral link lands on the database.

Decision How do we render and cache the question page for both anonymous readers and members?

Client-side rendering

Serve a static shell from the CDN.

Fetch the question, answers, and viewer state as JSON and render in the browser.

Pros

  • Shell is trivially cacheable.
  • One code path for all viewers.

Cons

  • Content waits for JavaScript: slow LCP, weaker SEO.
  • Every view still calls the API for the content.

Server render per request (no shared cache)

Render full HTML on each request, including the viewer's state.

Mark it private so no shared cache stores it.

Pros

  • Complete HTML, correct for everyone.
  • Simple mental model.

Cons

  • No edge caching for the anonymous majority of traffic.
  • Origin load scales with page views, not with writes.

Shared HTML at the edge, personal layer after

Render the question page without any personal data and cache it at the CDN for anonymous requests.

Members get the same HTML, then a small viewer-state request fills in votes, privileges, and the top bar.

Pros

  • Anonymous views are served from the edge in tens of milliseconds.
  • Origin work scales with edits, not views.
  • Personal data never enters a shared cache.

Cons

  • Members see a brief neutral state before their votes show.
  • Two code paths to test: anonymous and signed in.
  • Needs purging when a post changes.

Edge rendering with personalization at the edge

Cache the shared HTML at the edge, and let an edge function stitch in per-user fragments using the session cookie.

Pros

  • One response with everything; no flash of neutral state.

Cons

  • The edge needs the session and user data: more moving parts and more risk of leaking one user's data to another.
  • Harder to debug; vendor specific.

What we pick: Pick shared HTML at the edge with a personal layer after. The money page is served to anonymous readers from the CDN, members pay one extra small request, and nothing personal can ever be cached for the wrong person. Edge personalization is a later optimization if the neutral flash proves to matter.

How the split works

The edge does not need to look at the session cookie at all. A request without it gets the cached page, and a request with it gets the very same cached HTML; the browser then fetches viewer state. Both share one cache entry, as long as the HTML never changes with the cookie.

The rules from the CDN caching and HTTP caching lessons apply: cache by URL, leave cookies out of the cache key, and never set a cookie on a cached response.

http
GET /questions/11227809/why-is-processing-a-sorted-array-faster-than-processing-an-unsorted-array

HTTP/2 200
Content-Type: text/html; charset=utf-8
Cache-Control: public, max-age=60, s-maxage=600, stale-while-revalidate=60
Surrogate-Key: q-11227809 a-11227902 u-87234
Vary: Accept-Encoding

# Personal state is a separate, never-shared request:
GET /api/questions/11227809/viewer-state
Cookie: session=...

HTTP/2 200
Cache-Control: private, no-store
The page is public and tagged with surrogate keys for purging (the CDN strips that header before the browser sees it); viewer state is private and never stored.
Two sources make one question page. On the left, the shared HTML from the edge cache, public for 10 minutes: title, body, answers, comments, tags, a score that may be minutes old, and JSON-LD. On the right, the private viewer-state response, never stored by shared caches: your votes, privileges, own posts, fresh scores, and inbox and reputation counts. Both arrows point to the page in the browser at the bottom.shared HTML (edge, public)title, body, codeanswers, commentstags, author cardsscores, maybe staleJSON-LD, canonicalviewer-state (private)your votesprivilegesown postsfresh scoresinbox, reputationquestion page
The left box is the same bytes for every reader, so the CDN can serve it. The right box is about 1 KB, marked private, no-store, and fills slots that already exist in the HTML.

Three details make this work. First, surrogate keys (also called cache tags): the response lists the ids of every post and author on the page, so an edit to answer 11227902 can purge exactly the pages that show it. Second, short browser max-age, longer edge s-maxage: browsers keep the page for a minute, the edge for ten, and purges keep the edge fresh.

Third, stale-while-revalidate: when an entry expires, the edge may keep serving the old copy for up to 60 more seconds while it fetches a new one in the background, so a hot question never makes a reader wait for the origin.

Scores in the cached HTML may be a few minutes old. That is fine for an anonymous reader. A member's viewer-state response includes fresh scores for the posts on screen, so the numbers they act on are current.

What Stack Overflow itself is known for

Stack Overflow's own stack is famous for doing a lot with little. In Nick Craver's 2016 architecture post, the sites ran on 9 primary IIS web servers (plus 2 for meta and staging) running ASP.NET MVC, 4 SQL Servers, 2 Redis servers, and 3 tag engine servers, behind HAProxy load balancers.

On a sample day the load balancers saw about 209 million HTTP requests, 66 million of them page loads, and the 49 million question page renders that day averaged about 23 ms each. Caching was layered: an in-memory cache on each web server (L1) backed by Redis (L2). Craver also wrote that the sites had, unintentionally, run on a single web server a few times.

Our design adds an edge cache in front of that idea. The lesson is the same: when a page is read thousands of times per write, the fastest request is the one the origin never sees.

Viewer

Pick a viewer, then try an action. Unmarked parts are the shared HTML from the edge; marked parts come from the viewer-state request.

Question page seen by Anonymous. top bar: Log in, Sign up (shared), question up: up (shared), question down: down (shared), edit: Edit (shared), close: hidden, flag: Flag (shared), answer up: up (shared), answer down: down (shared), accept: hidden. Requests: page from the edge cache only./questions/11227809/...Log in, Sign upWhy is a sorted array faster?up27kdownquestion body, codeEditFlagup34kdownanswer bodyYour answer (sign up to post)plain: edgemarked: viewerpage: edge HITno viewer call0 KB private1 requestAnonymous
Pick a viewer, then try an action. Unmarked parts are the shared HTML from the edge; marked parts come from the viewer-state request.

Filling in the personal layer without layout shift

The viewer-state response is small and shaped for the page: the viewer's vote on each post, whether they bookmarked the question, their privileges, their unread counts, and fresh scores. The page applies it to buttons that already exist in the HTML. Because the arrows, the edit link, and the top bar slots are already there with fixed sizes, filling them in changes only colors and labels, not layout. That keeps Cumulative Layout Shift near zero.

Which parts become interactive? Only a few islands (small interactive parts of an otherwise static page): the vote column of each post, the comment area, the answer box, and the top bar. The question and answer bodies are plain HTML and never need to hydrate. The Islands and partial hydration lesson covers the technique; here it means a question page ships roughly 40 to 60 KB of compressed JavaScript for reading (our estimate), and loads the editor only when someone starts writing (see the performance chapter).

Check your understanding · Exercise 2

A member reports seeing another person's avatar in the top bar on a question page. Which mistakes could cause this? Select all that apply.

URLs and search: one canonical page per question

Search engines are the front door, so the URL and the HTML head are part of the frontend design. Three questions come up: what is the one true URL of a question, what do we tell Google about the page, and which pages should Google not index at all.

Id plus slug, and redirecting wrong slugs

Stack Overflow URLs look like /questions/{id}/{slug}. The id is the real key. The slug is the title turned into lowercase words joined by hyphens, there only for humans and search snippets, and it goes stale when the title is edited.

Links also get shared with no slug, with a truncated slug, or with tracking parameters. All of them must reach the page, and all of them must point Google to one URL.

Decision What should the server do with a request whose slug does not match the current title?

Serve the page at any slug

Look up by id, ignore the slug, render the page.

Pros

  • No extra round trip for old links.

Cons

  • Many URLs with the same content; search engines split ranking signals between them unless a canonical tag saves us.

Serve at any slug plus a canonical link tag

Render the page and add link rel=canonical with the current slug.

Pros

  • No redirect; Google usually merges duplicates.

Cons

  • Canonical is a hint, not a rule. Shared links keep the old slug in the address bar and in analytics.

Permanent redirect to the current slug, plus a canonical tag

If the slug differs, answer 301 with Location set to the canonical path.

On the canonical page, still emit link rel=canonical.

Pros

  • One URL in every address bar, cache, and index.
  • The CDN caches the redirect too.

Cons

  • One extra hop for stale links (cheap when the redirect is cached at the edge).

What we pick: Pick the 301 to the current slug plus a canonical tag. It costs one cached hop for stale links and gives every cache, every analytics report, and every search engine one URL per question. Short links like /q/11227809 use the same redirect.

ts
function resolveQuestionUrl(id: number, slug: string | undefined, q: QuestionMeta) {
  if (!q) return { status: 404 } as const;
  if (q.deleted && !q.viewerCanSeeDeleted) return { status: 404 } as const;
  const canonical = `/questions/${q.id}/${q.slug}`;
  if (slug !== q.slug) {
    return { status: 301, location: canonical } as const;
  }
  return { status: 200, canonical } as const;
}
Wrong or missing slugs redirect; the page itself also declares its canonical URL.
Sequence diagram with three lifelines: Reader, CDN edge, and Origin. The reader requests questions 11227809 with an old slug. The CDN misses and asks the origin, which looks up the id, finds the slug differs, and returns 301 with the canonical location. The CDN caches the 301 and passes it on. The reader follows it with GET of the canonical URL; the CDN has it cached and returns 200 HTML, a hit.ReaderCDN edgeOriginGET .../11227809/oldmiss: forward301 to canonical301, now cachedGET canonical URL200 HTML, HIT
Only the id is looked up; the slug is for humans. The redirect is the same for every reader, so the edge caches it, and the second hop is usually a cache hit too.

Sort and page parameters belong in the URL too (?answertab=trending, ?page=2), following the URL as state lesson. Page 2 of answers is a real page with its own canonical, because its answers are not on page 1. A sort change, though, shows the same answers in another order, so those URLs point their canonical at the default sort.

Structured data: QAPage

Google supports QAPage structured data (machine-readable facts about the page, usually a JSON-LD script in the head) from schema.org for pages where one question is followed by its answers and users can submit answers. That is exactly a question page. The page's mainEntity is a Question with its name (the title), text, answerCount, and its answers as acceptedAnswer and suggestedAnswer entries, each with text, upvoteCount, url, datePublished, and author.

Google's guidelines add three rules that matter here. The markup must contain the entire text of the question and of each answer. Comments go in comment, not as answers. FAQ pages and pages with several questions must not use it.

json
{
  "@context": "https://schema.org",
  "@type": "QAPage",
  "mainEntity": {
    "@type": "Question",
    "name": "Why is processing a sorted array faster than processing an unsorted array?",
    "text": "Here is a piece of C++ code that shows ...",
    "answerCount": 26,
    "upvoteCount": 27000,
    "datePublished": "2012-06-27",
    "author": { "@type": "Person", "name": "GManNickG", "url": "https://stackoverflow.com/users/..." },
    "acceptedAnswer": {
      "@type": "Answer",
      "text": "You are a victim of branch prediction fail ...",
      "upvoteCount": 34000,
      "url": "https://stackoverflow.com/a/11227902"
    },
    "suggestedAnswer": [{ "@type": "Answer", "text": "...", "upvoteCount": 4000, "url": "https://stackoverflow.com/a/..." }]
  }
}
JSON-LD in the head of the server HTML; numbers are rounded examples.

Generate it on the server from the same data that renders the page, so it can never disagree with what the reader sees. When there is more than one page of answers, answerCount stays the total while the markup lists only the answers on this page, which Google's documentation allows.

What not to index, and duplicates

  • Low-quality or unanswered new questions: a question with a negative score and no answers helps no one from search. In our design it gets meta name=robots content=noindex until it improves; the rule is server data, so the client never decides.
  • Deleted questions: return 404 to anonymous readers and crawlers. High-reputation members may still view deleted posts, so send that 404 with Cache-Control: no-store: the edge then passes every request for it to the origin, which checks the session and gives those members a private render. Deleted pages are rare, so the extra origin trips cost little.
  • Closed as duplicate: the page stays, with a banner that links to the original question at the top. The original is where search traffic should go, so duplicates can carry a canonical or noindex hint, per the site's policy. Many duplicates are kept on purpose: they are signposts with different wording that lead searchers to the one good answer.
  • User pages, tag listings, search results: listings change constantly and repeat content found on question pages. Search result pages in particular should be noindex.

Check your understanding · Exercise 3

A question's title was edited, and its old URL is linked from thousands of blog posts. Which statements are true for our design? Select all that apply.

Ordering and paging answers

Most questions have a handful of answers. A few famous ones have dozens, and the order decides which code thousands of people paste into their projects. Getting the order right is a product decision; keeping it stable while people vote is a frontend one.

The sorts, and the accepted answer

Stack Overflow offers a sort menu above the answers: highest score (the default), trending (recent votes count more), date modified (newest first), and date created (oldest first). For years the accepted answer, the one the asker marked with a green check, was pinned to the top whatever its score.

In 2021, as part of its work on outdated answers, Stack Overflow ran an A/B test and then made the change permanent: the accepted answer is no longer pinned, and it sorts by score like every other answer. The reason was simple. Askers accept the answer that fixed their problem in 2012; the community may later upvote a better, current one far above it.

So the sort is a server concern with a URL handle: a query parameter such as ?answertab=trending. The server returns answers already sorted, the URL is shareable, and the CDN caches each sort as its own entry. The client never re-sorts by itself, because it does not have the data (trending weights recent votes, which only the server knows).

Paging answers

Stack Overflow shows up to 30 answers per page and adds page links when a question has more. Since almost every question fits on one page, we do not need infinite scroll or cursors here: plain numbered pages with ?page=2 are simpler, cacheable, and linkable, as the Pagination lesson explains. Offset paging is fine because the list is short and changes slowly.

One trap: a link to a specific answer, like /a/11227902, must land on the page that holds it. The short link redirects to /questions/{id}/{slug}/{answerId}#{answerId} and the server computes which page the answer is on for the default sort. The fragment then scrolls to it.

Keeping the order stable while people vote

Picture a member reading answer 3 of 8. They upvote it, and its score passes answer 2. If the client re-sorted on every vote, the answer they are reading would jump up the page under their cursor. The same happens when viewer state arrives with fresh scores, or when another reader's vote comes in live.

Decision When scores change on the page, what happens to the order?

Re-sort live

Every score change re-sorts the list in the client.

Pros

  • Order always matches scores.

Cons

  • Content jumps while reading: bad layout shift, lost place, and screen readers lose focus.

Freeze the order for the page view

Keep the server's order until the next navigation; update only the numbers.

Pros

  • Nothing moves under the reader.
  • Simple: order is part of the cached HTML.

Cons

  • For a few minutes, a 41 may sit above a 42. Nobody notices.

Freeze, plus an explicit refresh

Keep the order; when new answers arrive, show a notice the reader clicks to load them.

Pros

  • The reader chooses when the page changes.
  • Matches how Stack Overflow announces new answers.

Cons

  • One more piece of UI and state.

What we pick: Pick freeze, plus an explicit refresh. The order comes from the server render and stays put for the page view. Scores update in place. New answers wait behind a notice (covered in the live updates chapter).

Two columns compare what happens after the reader upvotes answer C from 41 to 42 while reading it. Left, re-sort live: the list becomes A 45, C 42, B 41, so C jumps up past B under the reader's eyes, and the reader loses their place. Right, frozen order: the list stays A 45, B 41, C 42 with only the number changed, and a banner offers 1 new answer to load.re-sort liveA 45C 42 (jumped up)B 41reader loses their placelayout shift, lost focusfrozen orderA 45B 41C 42 (in place)1 new answer: loadonly numbers change
Freezing the order means the number changes but the text the reader is looking at does not move. The order catches up on the next page view, or when the reader clicks the banner.

Check your understanding · Exercise 4

Which statements about answer ordering in this design are true? Select all that apply.

Voting: Stack Overflow's rules on top of optimistic votes

The mechanics of an up/down toggle with an optimistic update and a rollback are in the Reddit guide and the Optimistic updates lesson. We will not repeat them. What Stack Overflow adds is a set of rules about who may vote, what a vote costs, and when a vote becomes final. Each rule shows up in the UI.

Rule (from the Stack Overflow help center)What the client does
Voting up needs 15 reputation; voting down needs 125.Arrows render for everyone. Below the threshold, a click opens a short explanation instead of voting.
You cannot vote on your own posts.The viewer-state marks own posts; their arrows are disabled with a reason.
Upvotes give the author +10. Downvotes take 2 from the author. Downvoting an answer costs the voter 1; downvoting a question is free.The client shows the voter's own change (a -1 toast) only after the server confirms. It never computes reputation.
30 votes per UTC day, plus 10 more on questions only.The server returns the remaining count; the client warns near the limit and shows the server's message when it is reached.
A vote can be undone only within a short time, then it locks until the post is edited.After the window the active arrow stays pressed; clicking it shows the lock message from the server.
Reputation from votes is capped at 200 a day; accepts and bounties are not.Server only. The client never shows a gain the server did not report.

The pattern in that table: every rule lives on the server, and the client gets just enough to predict the result and explain a refusal. The viewer-state response carries the viewer's reputation, their privileges, the posts they own, their current votes, and their votes left today. That is enough to disable the right arrows before anyone clicks, which avoids most rollbacks.

The vote flow, including refusals

A click on an enabled arrow updates the score and the pressed state at once, then sends POST /api/posts/{id}/vote with the new vote type. Three outcomes: success (keep the change, use the server's score), refusal (roll back and show the server's reason next to the arrow), or network failure (roll back and offer a retry). A refusal is normal here, not an error. The vote may be locked, the daily limit may be used up, or another tab may have voted already.

Sequence diagram with four lifelines: Member, Vote island, Viewer store, and API. First, the member clicks up; the island sets the vote to up and the score to 42 in the store and sends POST vote up; the API replies ok with score 42 and 29 votes left; the island keeps it. Later, the member clicks up again to undo; the store is set back to none, 41; the API replies 409 locked; the island rolls back to up, 42 and shows the message: vote locked until the post is edited.MemberVote islandViewer storeAPIclick upup, score 42POST vote upok, 42, 29 leftclick up (undo)none, 41POST vote none409 lockedroll back: up, 42
The first click is the happy path. The second shows the Stack Overflow specific failure: a refusal the server explains. The island rolls back and shows the reason next to the arrow, instead of a generic error.
ts
async function castVote(postId: number, next: VoteType) {
  const prev = votes.get(postId);
  applyVote(postId, next); // optimistic: arrow + score
  try {
    const res = await api.vote(postId, next);
    if (res.ok) {
      setScore(postId, res.score);
      setVotesLeft(res.votesLeft);
      return;
    }
    applyVote(postId, prev); // refused: roll back
    showVoteMessage(postId, res.reason); // "locked", "daily-limit", "own-post"
  } catch {
    applyVote(postId, prev);
    showVoteMessage(postId, "network");
  }
}
Optimistic vote with rollback; the refusal reason comes from the server and is shown next to the arrow.

Two details matter. Rapid clicks (up, then down, then up) should not send three racing requests: keep one request in flight per post and send only the latest intent when it returns. And the score shown after success should be the server's number, not our local guess, because other people vote too.

State machine for a vote. From idle, a click moves to pending with the optimistic change. From pending, ok goes back to idle with the server score; refused or network error goes to rolled back, which shows the reason and returns to idle. From idle, when the undo window passes, the vote becomes locked; an edit to the post unlocks it back to idle.idlependingrolled backlockedclickokrefused, errorshow reasonwindow passesedited
A click while pending is not a new request: the island remembers only the latest intent and sends it when the current request returns. Locked is a real state, so the arrow can explain itself instead of failing.

Accepting an answer

Accepting is not a vote. Only the asker sees the check outline on each answer, only one answer can be accepted at a time, and accepting a different one moves the check. It gives the answerer +15 and the asker +2.

The UI is a toggle group with one choice, using the same optimistic pattern as votes. Anonymous readers see only the filled check on the accepted answer, which is part of the cached HTML.

Check your understanding · Exercise 5

A member with 80 reputation clicks the down arrow on an answer. What should happen in this design? Select all that apply.

The ask flow: catch duplicates before they are posted

Every duplicate question costs the community time: someone reads it, finds the original, and votes to close. The cheapest place to stop a duplicate is the ask form, while the person is typing the title. The second goal is quality: a question with a clear title, some code, and the right tags gets answered; one without them gets closed.

The form and its rules

  • Title: 15 to 150 characters, specific enough to stand alone in a search result.
  • Body: markdown, at least 30 characters and at most 30,000, usually with a code block. Edited in the markdown editor with a live preview (the Markdown editor guide covers the editor, the preview, and sanitizing).
  • Tags: 1 to 5, chosen from existing tags with autocomplete (the Tag input guide covers the chips and keyboard). Creating a new tag needs 1,500 reputation, so most users pick from the list.
  • Guidance: Stack Overflow's ask page walks new askers through what a good question contains. Since 2024, some first questions from new users go to the Staging Ground, a private space where experienced reviewers give feedback before the question goes public; how many are routed there depends on reviewer capacity.

The client checks these rules live for feedback, and the server checks them again on submit (see the Input validation lesson). Counting characters sounds trivial, but the server counts the raw markdown, so the client must count the same string, not the rendered preview.

Similar questions while typing

Decision When do we look for similar questions?

Only on submit

Run a duplicate search when the form is submitted and show matches on an interstitial page.

Pros

  • One search per question.

Cons

  • The asker has already written the body; they are unlikely to throw it away now.

On title blur

Search once when focus leaves the title field.

Pros

  • Few requests; results show before the body is written.

Cons

  • Many people type the title and press Tab quickly; the panel appears late and is missed. Later edits to the title are not checked.

Debounced while typing the title

After the title passes 15 characters, wait for a pause of about 400 ms, cancel the previous request, and search.

Pros

  • Suggestions appear while the asker is still thinking about the title.
  • Stays current as the title changes.

Cons

  • More requests; needs cancelling and race handling.

What we pick: Pick debounced while typing, with a minimum length and request cancellation. The search request is cheap compared with a duplicate post, and the Debounce and throttle lesson gives the timing. We also run one final check on submit for people who pasted a title and never paused.

ts
let timer: ReturnType<typeof setTimeout> | undefined;
let inflight: AbortController | undefined;

function onTitleInput(title: string) {
  clearTimeout(timer);
  if (title.trim().length < 15) {
    inflight?.abort();
    return setSimilar([]);
  }
  timer = setTimeout(async () => {
    inflight?.abort();
    inflight = new AbortController();
    try {
      const res = await fetch(`/api/similar?title=${encodeURIComponent(title)}`, { signal: inflight.signal });
      setSimilar(await res.json());
    } catch (e) {
      if ((e as Error).name !== "AbortError") setSimilar([]);
    }
  }, 400);
}
Debounce the title, abort the stale request, ignore the abort error.

Aborting matters because responses can come back out of order: the search for "react useEffect runs" may return after the one for "react useEffect runs twice" and overwrite better results. Cancelling the old request with AbortController removes the race. The suggestions panel is a list of links, opened in a new tab so the draft is not lost, with each result's score and whether it has an accepted answer. The Autocomplete guide covers the same pattern in more depth.

Sequence diagram with four lifelines: Asker, Title field, Debouncer, and API. The asker types useEffect; the field restarts the 400 millisecond timer. After a pause, the debouncer sends GET similar number 1. The asker types more: runs twice. The debouncer aborts the first request and, after the next pause, sends GET similar for the full title. The API replies with 3 matches and the panel shows them. The aborted request never updates the panel.AskerTitle fieldDebouncerAPIkeys: useEffectrestart 400 msGET similar #1keys: runs twiceabort #1GET similar #23 matchesshow panellinks, new tab
Every keystroke restarts the timer; every new search aborts the old one with AbortController. Only the reply for the current title can reach the panel.

Drafts that survive a closed tab

Writing a good question can take many minutes, and people switch tabs to test code. Losing the draft to a crash or a stray click is the worst outcome on the ask page. So we save the title, body, and tags to localStorage under a key like draft:ask, throttled to once every 2 seconds while typing and immediately on visibilitychange to hidden.

On load, a saved draft is restored with a notice and a discard button. On successful submit, the draft is deleted. Answers get the same treatment, keyed by question id (draft:answer:11227809).

Should drafts sync to the server? It helps people who switch devices, but it means storing unpublished text and writing an endpoint for it. A local draft covers the common failure (a closed tab) with no backend work. If the interviewer asks for cross-device drafts, add a server save every 30 seconds with the local copy as a fallback.

Type a question title. Searches start after a 400 ms pause, once the title has 15 characters.

Ask flow lab. 0 keystrokes in the title. Requests: none yet. Similar questions: none. Title 0 characters, needs 15 to 150. Body 0 characters, needs at least 30. 0 tags, needs 1 to 5. Draft saved 0 times.requests (last 5)similar questionslive validationtitle 0/15-150body 0/30+tags 0/1-5draft saved 0 timesat most every 2 s, localStoragekeys 0, sent 0aborted 0, stale 0
Type a question title. Searches start after a 400 ms pause, once the title has 15 characters.

Check your understanding · Exercise 6

The similar-questions panel sometimes shows results for an older version of the title. Which fixes address the cause? Select all that apply.

Reputation gates, editing, and suggested edits

Reputation turns one page into many. The same post shows a plain "Edit" to a 2,000-reputation member (applied at once), an "Edit" that becomes a suggestion to a 50-reputation member, a "Close" link to a 3,000-reputation member, and none of that to a visitor. Here are the thresholds that change the question page, from the help center's privileges list.

ReputationPrivilegeEffect on the question page
1Create posts, comment everywhereAnswer box and comment links for every member. (Commenting on other people's posts needed 50 until October 2025.)
15Vote up, flag postsUp arrows and the flag link work.
125Vote downDown arrows work; answer downvotes cost 1.
250View close votesSee and cast close and reopen votes on your own questions.
1,000Established userCan see the separate up and down vote counts on a post.
2,000Edit postsEdits apply at once and skip review; can review suggested edits.
3,000Cast close and reopen votesClose link on any question, including "duplicate of".
10,000Moderator toolsSee deleted posts; vote to delete questions.

These numbers change (in October 2025 the comment threshold dropped from 50 to 1), so the client must not hard-code them. The viewer-state response sends a list of privilege flags (canVoteDown, canEditDirectly, canClose), and a separate, cacheable config sends the thresholds for the explanation text. The server still checks every action; the client only predicts and explains.

Decision How do we show an action the viewer cannot take yet?

Hide it

Render only the actions the viewer may take.

Pros

  • Clean UI.

Cons

  • New users never learn the feature exists, and the page looks different for no visible reason.

Disable it silently

Render a greyed-out button with disabled.

Pros

  • Shows the feature exists.

Cons

  • No reason given; disabled buttons are skipped by keyboard focus, so screen reader users may never find it.

Show it, explain on use

Keep the control focusable with aria-disabled=true; on click, show the requirement ("Voting down needs 125 reputation; you have 80").

Pros

  • Teaches the system; works with keyboard and screen readers.

Cons

  • Takes a little more UI work per action.

What we pick: Show it, explain on use for actions that are part of everyday reading (votes, comments, edits). Hide moderator-only tools entirely, because they would only add noise for the 99% who will never earn them.

Edits, suggested edits, and conflicts

Clicking "Edit" opens the post's markdown in the same editor used for answers, plus an edit summary field. Below 2,000 reputation (except on your own posts, which you can always edit directly) the submit button says "Submit suggestion" and the edit goes to a review queue, where members with the edit privilege approve or reject it; two votes in either direction decide.

Until then, the post page shows the original to everyone and a pending note to the suggester. An approved suggestion gives the suggester +2 reputation, up to 1,000 in total.

Edit conflicts are real on popular posts. The general patterns (base revision ids, "your edit conflicts with a newer revision", three-way merge) are covered in the Wikipedia guide, and the revision history with its diff view is covered there and in the Code review diff guide.

The Stack Overflow specific part is small. Send the revision id you started from; if the server says a newer revision exists, keep the user's text, show a notice that the post was edited, and offer to open the new version side by side. Also, while an answer is being written, a live notice tells the writer if the question itself was edited, because the question may now ask something different.

Check your understanding · Exercise 7

Stack Overflow lowers the reputation needed for some privilege. What must change in our client for the UI to follow? Select all that apply.

Live updates, comments, and moderation

A question page is mostly static, but a fresh question is busy: in the first hour it may get several answers, comments asking for details, and edits by the asker. People who are writing an answer need to know when someone else already posted the same fix. Stack Overflow has pushed such events over WebSockets for many years.

Nick Craver's 2016 post says the sites pushed top-bar notifications, vote counts, new answers, and new comments this way, through NetGain, their own WebSocket server library. At peak they held about 500,000 concurrent connections, and he called sockets far more efficient than polling at that scale.

One socket, a few small events

Each tab opens one WebSocket (see the WebSockets lesson) and subscribes to topics: the question it is showing (question:11227809) and, for members, their own account (user:87234). The server pushes tiny events, not content: answer-added, post-edited, comment-added, score-changed, inbox-count, reputation-change. The client decides what to fetch. Anonymous readers can skip the socket entirely on old questions, since a ten-year-old page gets few changes; we only connect when the question is recent or the reader starts writing an answer.

  • New answer: never insert it into the list. Show a banner above the answers, "1 new answer", that loads it on click. This keeps the frozen order from the answer chapter and does not move what the reader is looking at.
  • Post edited: if the reader is not editing, show "This post was edited" with a link to reload it in place. If they are editing it, warn them before they save.
  • Score changed: update the number in place, no movement.
  • Inbox and reputation: update counters in the top bar; a reputation change shows a small toast like "+10" (the Notifications center guide covers the inbox).

When the socket drops (a laptop sleeps), reconnect with backoff and, on reconnect, ask once for "what changed since revision X" so nothing is missed. Pages in a background tab can pause and catch up when the tab becomes visible.

State machine for the realtime connection. Connecting moves to subscribed when the socket opens and the page subscribes to question and user topics. Subscribed moves to backoff when the connection drops; backoff waits 1, 2, 4, then up to 30 seconds and reopens the socket into catching up, which asks for changes since the last revision, applies them, and returns to subscribed.connectingsubscribedbackoffcatching upopen, subscribedropreopenedwait 1, 2, 4 ... 30 smissed events applied
Reconnecting is not enough: the page must also ask what it missed while offline, or a new answer posted during the gap is never announced.

Comments: short, shallow, and secondary

Comments on Stack Overflow are not discussion threads in the forum sense. They are short notes (15 to 600 characters) under a post, with upvotes but no downvotes, and no revision history. Since October 2025 any member can comment and vote on comments, and a reply can sit directly under the comment it answers, so short exchanges stay together.

A post with many comments shows only some of them, with a link to show the rest. Since the lists are short and shallow, the client needs far less than the deep tree in the Comments thread guide: a list with replies, an inline form, and the same optimistic insert with rollback. Comments render a tiny markdown subset (bold, italic, code, links), sanitized on the server.

Flags, close votes, and duplicates

Flagging and closing are dialogs with a reason picker. A common close reason is "duplicate of": the member pastes a URL or searches for the original, and the dialog shows the candidate's title and score to confirm. Close votes count toward a threshold, and when it is reached, the question shows a closed banner.

For a duplicate, the banner sits at the top of the question and links the original, which is the main thing an anonymous reader needs. Since that banner changes the shared HTML, closing purges the question's surrogate key.

Check your understanding · Exercise 8

A member is halfway through writing an answer when another answer arrives. What should the page do? Select all that apply.

Architecture, data model, and API

Now put the pieces together. The design has a server-rendered page, a CDN in front of it, a few client islands, and three channels for data: the cached HTML, the private viewer-state request, and the socket. Each boundary exists because of one of the four traits from the start of the guide.

PartResponsibilityWhy it is separate
Page renderer (server)Renders the question page without personal data: post HTML, tags, author cards, JSON-LD, canonical tags.Shared output is what makes the page cacheable.
CDN edgeCaches pages and redirects by URL; purges by surrogate key on writes.Absorbs the anonymous majority of traffic.
Viewer-state APIReturns votes, privileges, own posts, fresh scores, and counters for one viewer and one page.Personal, private, never cached by shared caches.
Write APIVotes, accepts, answers, comments, edits, flags, close votes.Every rule is enforced here.
Realtime gatewayOne WebSocket per tab; topic subscriptions; small events.Keeps push separate from page rendering.
Vote and accept islandsHydrated controls on each post; optimistic writes.The only interactive part most readers touch.
Composer islandMarkdown editor, preview, draft saving; loaded on demand.The heaviest code; most readers never need it.
Viewer storeA small client store for viewer state, keyed by post id.One place for islands to read votes and privileges.
Architecture in three zones. Browser: page HTML, vote and comment islands, a lazy composer (dashed), a viewer store, and a socket client. Edge: a CDN cache. Origin: page renderer, write API, viewer-state API, and realtime gateway. Arrows: page renderer to CDN cache to page HTML; islands to write API; a curved arrow from the write API back up to the CDN cache for purges; viewer-state API to viewer store; realtime gateway to socket client to viewer store.Browserpage HTMLvote, comment islandscomposer (lazy)viewer storesocket clientEdgeCDN cacheOriginpage rendererwrite APIviewer-state APIrealtime gateway
Three channels feed the page: shared HTML through the edge, private viewer state straight from the origin, and small events over the socket. The curved arrow on the right is the purge an edit sends to the CDN. Only the composer is heavy, and it loads when someone starts writing.

Why no big client framework state for the posts themselves? Post bodies are already HTML from the server and never change on the client except through a reload of that post. Only the small, personal, interactive bits need a store. The islands share it, so the vote column and the top bar agree.

Data flow for the question page. Top row: the posts database sends rows to the renderer, which sends HTML to the CDN, which sends HTML to the page. Middle row: a click in the page reaches the vote island, which sends POST vote or edit to the write API; the write API saves to the database and, for edits, purges the question's key at the CDN. Bottom row: the viewer-state API sends private JSON to the viewer store, which gives the island the viewer's vote and fills slots in the page.postsrendererCDNpagerowsHTMLHTMLwrite APIvote islandclickPOST vote, editsavepurge on editviewer-state APIviewer storeprivate JSONyour votefill
Reads of shared content go left to right through the edge. Personal data skips the edge. Writes go back to the database, and only the writes that change the shared HTML send a purge.

Follow a write through the diagram. A member upvotes: the island changes its state, the write API records the vote and returns the new score, and the server publishes score-changed on the question topic. Other open tabs update their number.

The cached HTML still has the old score for up to its time to live, which is fine for anonymous readers. An edit is different: it changes the shared HTML, so the server purges the question's surrogate key and the next anonymous request gets a fresh render.

Check your understanding · Exercise 9

Which writes should purge the cached question page at the edge? Select all that apply.

Data model: what the client holds

The client holds very little data, and most of it never needs a store. The split follows the cache split: shared data arrives as HTML, personal data arrives as JSON, and unsent text lives in local storage.

DataWhere it livesWhy
Question, answers, comments, tags, authorsServer HTML (cached at the edge); bodies never parsed on the clientShared by everyone; already rendered.
Viewer state: votes, own posts, privileges, votes leftViewer store in memory, filled from viewer-statePersonal and short-lived; refetched each page view.
Live scores and countersViewer store, updated by socket eventsSmall numbers that change in place.
Sort and answer pageURL query stringShareable, cacheable, and works with back and forward.
Drafts (ask, answer, edit)localStorage, keyed by kind and post idSurvives a closed tab with no backend work.
Similar question resultsComponent stateOnly matters while the ask form is open.
ts
type VoteType = "up" | "down" | "none";

type ViewerState = {
  userId: number;
  reputation: number;
  privileges: { canVoteUp: boolean; canVoteDown: boolean; canComment: boolean; canEditDirectly: boolean; canClose: boolean };
  votesLeft: { any: number; questionsOnly: number };
  posts: Record<number, {
    vote: VoteType;
    voteLocked: boolean;
    isOwn: boolean;
    score: number; // fresh, replaces the cached number
  }>;
  acceptable: boolean; // viewer asked this question
  unread: { inbox: number; reputation: number };
};

type Draft = { kind: "ask" | "answer" | "edit"; key: string; title?: string; body: string; tags?: string[]; baseRevision?: number; savedAt: number };
The client-side types: everything personal fits in one small object per page.

Endpoints

http
GET  /questions/{id}/{slug}?answertab=&page=   -> HTML (public, edge cached)
GET  /api/questions/{id}/viewer-state?page=     -> ViewerState (private, no-store)
POST /api/posts/{id}/vote      { type }         -> { ok, score, votesLeft } | { ok: false, reason }
POST /api/answers/{id}/accept  { accepted }     -> { ok, acceptedAnswerId }
POST /api/questions/{id}/answers { body }       -> { answerId, html }
POST /api/posts/{id}/comments  { text }         -> { commentId, html }
POST /api/posts/{id}/edits     { body, title?, tags?, summary, baseRevision }
                                                -> { applied: true, revision } | { suggested: true } | { conflict, latestRevision }
GET  /api/similar?title=                        -> [{ id, title, score, answered, url }]
POST /api/questions            { title, body, tags } -> { id, url } | { errors: { field, message }[] }
WS   /realtime  subscribe question:{id} | user:{id}  -> { type, postId?, revision?, score? }
Write endpoints return the server's numbers and a machine-readable reason on refusal.

Two shapes are worth defending in an interview. First, the refusal format { ok: false, reason } with reasons like locked, daily-limit, own-post, or rep-required: these are expected outcomes, so they come back as data with a 4xx status, and the client maps each to a sentence. Second, answers and comments return server-rendered html. The client shows the post exactly as everyone else will see it, sanitized once on the server (the XSS lesson), and never has to trust its own preview.

Think first

Why does viewer-state take the answer page number instead of a list of post ids?

Check your understanding · Exercise 10

Where should the current answer sort live? Select all that apply.

Performance, code blocks, and accessibility

The question page is read far more than it is written, so performance work goes to the reading path. The Core Web Vitals lesson defines the targets; here is how this page meets them.

A budget for the reading path

MetricTargetWhat gets us there
Time to first byteUnder 200 ms for anonymous viewsEdge cache hit; stale-while-revalidate on expiry.
Largest Contentful PaintUnder 2.5 s on a mid-range phoneThe question title and body are in the HTML; one font file preloaded or a system font stack; no client render.
Interaction to Next PaintUnder 200 msOnly small islands hydrate; the editor is not on the page until needed.
Cumulative Layout ShiftUnder 0.1Fixed slots for viewer state, ads, and images with known width and height; new answers behind a banner.
JavaScript for readingAbout 40 to 60 KB compressed (our estimate)Votes, comments, top bar, socket client. The highlighter runs only if code blocks exist.

Defer the editor. The markdown editor, its preview renderer, and image upload can easily weigh more than 100 KB compressed. Most readers never write. So the "Your answer" area renders as a plain-looking textarea in the HTML, and the full editor is loaded with a dynamic import() when the reader focuses it, scrolls near it, or clicks "Answer" (the Code splitting lesson). The textarea keeps what they typed while the editor loads.

Highlight code in the right place. Stack Overflow replaced Google Prettify with highlight.js in 2020, running in the browser, and highlight.js guesses the language when the tags do not say. Highlighting a long page can take noticeable main thread time. Two good choices: highlight on the server once per revision and cache the HTML (no client cost), or highlight on the client only the blocks near the viewport, in idle time. Either way, the code text is in the HTML first, so it is readable before colors arrive.

Images and third parties. Post images are served from Stack Exchange's own image host (i.sstatic.net, which replaced the long-running i.stack.imgur.com hosting in a migration across 2023 and 2024). Give them width and height from upload metadata and loading=lazy below the fold.

Ads and analytics are the main third-party scripts: load them after the content, in reserved slots so they cannot shift text, as the Third-party scripts lesson describes. Members with 200 reputation see fewer ads, so the cached HTML holds only empty, fixed-size slots, and viewer state decides which of them load an ad.

Code blocks people copy from

  • Never wrap code. Wrapped code changes meaning in Python and YAML and is hard to read everywhere. Use white-space: pre and horizontal scroll inside the block, so the page itself never scrolls sideways.
  • Limit height. Very long blocks get a max height with their own vertical scroll, so a 400-line log does not push the answers off screen.
  • Copy button. One button per block using navigator.clipboard.writeText, with a visible "Copied" state and an aria-live announcement. Copy the raw text, not the highlighted HTML.
  • Scrollable regions are focusable. A block that scrolls sideways gets tabindex=0 and a label, so keyboard users can scroll it with arrow keys.

Accessibility of votes and actions

The vote column is the most used control, and it is easy to build badly with icons alone. Each arrow is a real button with an accessible name that includes the post ("Up vote answer by Mysticial") and aria-pressed for the viewer's vote.

The score between the arrows is plain text, and a visually hidden live region announces the result after a vote ("Upvoted. Score 42") or the refusal reason. Locked actions use aria-disabled so they stay focusable and can explain themselves, following the ARIA patterns lesson.

Stack Exchange also has keyboard shortcuts as an opt-in preference in the user settings, with ? showing the list. Shortcuts are opt-in for a reason: single-key shortcuts can clash with screen reader keys and with typing, which WCAG's character key shortcuts rule warns about. The banner for new answers is announced politely, and loading it moves focus to the first new answer's heading.

Check your understanding · Exercise 11

The question page's Interaction to Next Paint is poor on long pages with many code blocks. Which changes help? Select all that apply.

Running it in the interview

Forty-five minutes is enough for the question page and one write flow in depth. Spend them like this, and say the reasons out loud: the interviewer is grading your trade-offs, not your diagram.

MinutesWhat to doWhat to say
0 to 5Ask who reads the question page and how rules depend on the user."If most views are anonymous search traffic, the cache strategy drives everything."
5 to 15Draw the question page wireframe and the cache split: shared HTML at the edge, viewer state after."Nothing personal goes in the cached HTML. Members get one small private request."
15 to 25Voting and privileges: rules from the server, predicted and explained in the UI, optimistic with rollback."Refusals are normal outcomes, not errors."
25 to 35The ask flow: debounced similar-question search with cancellation, validation, local drafts."The cheapest duplicate is the one never posted."
35 to 45SEO, live updates, performance, accessibility; answer follow-ups."Edits purge the page by surrogate key; votes do not."

Draw the wireframe first, then the architecture. A wireframe with the vote column, the post body, comments, the answer box, and the top bar gives you something to point at when you explain which parts are cached and which are personal. Avoid spending ten minutes on the markdown editor: say it is a solved component, link it to the preview and sanitizer, and move on.

Wireframe of a Stack Overflow question page. A top bar with the logo, search, and the viewer's inbox and reputation. The question title with asked, modified, and viewed counts. A question row with a vote column (up arrow, score, down arrow, bookmark) beside the body with code, tags, edit and close links, the author card, and comments. A sort menu for answers set to highest score. An answer row with its own vote column and an accepted check. At the bottom, the Your answer box. A right sidebar with linked and related questions.stackoverflow.com/questions/11227809/...top bar: logo, search, inbox, reputationquestion titleup42downbody with code blocktagsedit, close, flagcomments, add a commentsort: highest scoreup57checkanswer bodyYour answer (editor loads on focus)linkedrelated
Name the parts before you design them. The vote columns, comment links, answer box, and the top bar are the only interactive islands; everything else is plain server HTML.

Follow-ups to expect

Think first

"Members see neutral arrows for a moment before their votes appear. Can you remove that?"

Think first

"A question goes viral on social media while people are actively answering it. What breaks?"

Check your understanding · Exercise 12

Which statements would a senior candidate make about this design? Select all that apply.

Summary

  • The question page is the money page: most views are anonymous search visitors, so it is server rendered and cached at the edge with nothing personal in it.
  • Members get the same HTML plus one small private viewer-state request for votes, privileges, fresh scores, and counters, filled into fixed slots so nothing shifts.
  • One canonical URL per question: the id is the key, wrong slugs answer 301, and QAPage structured data is generated from the same data as the page.
  • Answer order comes from the server and stays frozen for the page view; the accepted answer is no longer pinned on Stack Overflow, and new answers wait behind a banner.
  • Reputation rules live on the server. The client gets privilege flags to predict and explain them, shows locked actions with a reason, and treats refusals as normal outcomes.
  • The ask flow searches for similar questions while the title is typed, debounced and cancelled with AbortController, and keeps drafts in localStorage.
  • Edits, new answers, and closures purge the cached page by surrogate key; votes do not.
  • The reading path stays light: the editor loads on demand, code never wraps, and vote buttons use aria-pressed with announced results.

Check your answer

Tick what your own design covered. Pick the level you're interviewing for: each level includes the ones before it.

Junior 0 of 4
Mid-level 0 of 5

References

Concepts behind it

Try next