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.
| Question | Answer we assume | What 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.
- 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.
- 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.
- 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.
- 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?
Rendering every view means every anonymous reader waits for a database read, markdown rendering, and HTML templating, and the servers do the same work millions of times for identical output.
Caching the whole page with member buttons means either one cache entry per member (useless: most entries are read once) or, worse, serving one member's vote state and edit buttons to someone else. The answer is to split the page: a shared part cached for everyone and a small personal part fetched per viewer.
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?
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.
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
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.
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?
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.
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;
}
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.
{
"@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/..." }]
}
}
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=noindexuntil it improves; the rule is server data, so the client never decides. - Deleted questions: return
404to anonymous readers and crawlers. High-reputation members may still view deleted posts, so send that404withCache-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?
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).
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.
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");
}
}
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.
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?
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.
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);
}
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.
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.
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.
| Reputation | Privilege | Effect on the question page |
|---|---|---|
| 1 | Create posts, comment everywhere | Answer box and comment links for every member. (Commenting on other people's posts needed 50 until October 2025.) |
| 15 | Vote up, flag posts | Up arrows and the flag link work. |
| 125 | Vote down | Down arrows work; answer downvotes cost 1. |
| 250 | View close votes | See and cast close and reopen votes on your own questions. |
| 1,000 | Established user | Can see the separate up and down vote counts on a post. |
| 2,000 | Edit posts | Edits apply at once and skip review; can review suggested edits. |
| 3,000 | Cast close and reopen votes | Close link on any question, including "duplicate of". |
| 10,000 | Moderator tools | See 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?
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.
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.
| Part | Responsibility | Why 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 edge | Caches pages and redirects by URL; purges by surrogate key on writes. | Absorbs the anonymous majority of traffic. |
| Viewer-state API | Returns votes, privileges, own posts, fresh scores, and counters for one viewer and one page. | Personal, private, never cached by shared caches. |
| Write API | Votes, accepts, answers, comments, edits, flags, close votes. | Every rule is enforced here. |
| Realtime gateway | One WebSocket per tab; topic subscriptions; small events. | Keeps push separate from page rendering. |
| Vote and accept islands | Hydrated controls on each post; optimistic writes. | The only interactive part most readers touch. |
| Composer island | Markdown editor, preview, draft saving; loaded on demand. | The heaviest code; most readers never need it. |
| Viewer store | A small client store for viewer state, keyed by post id. | One place for islands to read votes and privileges. |
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.
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.
| Data | Where it lives | Why |
|---|---|---|
| Question, answers, comments, tags, authors | Server HTML (cached at the edge); bodies never parsed on the client | Shared by everyone; already rendered. |
| Viewer state: votes, own posts, privileges, votes left | Viewer store in memory, filled from viewer-state | Personal and short-lived; refetched each page view. |
| Live scores and counters | Viewer store, updated by socket events | Small numbers that change in place. |
| Sort and answer page | URL query string | Shareable, cacheable, and works with back and forward. |
| Drafts (ask, answer, edit) | localStorage, keyed by kind and post id | Survives a closed tab with no backend work. |
| Similar question results | Component state | Only matters while the ask form is open. |
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 };
Endpoints
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? }
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?
The server already knows which posts are on page 2 for that sort, so the request is short and cannot be used to probe arbitrary posts. It also lets the server include fresh scores for exactly the posts on screen.
A list of ids works too, but long id lists make long URLs, and the client would have to read them from the HTML first, which delays the request.
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
| Metric | Target | What gets us there |
|---|---|---|
| Time to first byte | Under 200 ms for anonymous views | Edge cache hit; stale-while-revalidate on expiry. |
| Largest Contentful Paint | Under 2.5 s on a mid-range phone | The question title and body are in the HTML; one font file preloaded or a system font stack; no client render. |
| Interaction to Next Paint | Under 200 ms | Only small islands hydrate; the editor is not on the page until needed. |
| Cumulative Layout Shift | Under 0.1 | Fixed slots for viewer state, ads, and images with known width and height; new answers behind a banner. |
| JavaScript for reading | About 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: preand 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 anaria-liveannouncement. Copy the raw text, not the highlighted HTML. - Scrollable regions are focusable. A block that scrolls sideways gets
tabindex=0and 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.
| Minutes | What to do | What to say |
|---|---|---|
| 0 to 5 | Ask 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 15 | Draw 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 25 | Voting and privileges: rules from the server, predicted and explained in the UI, optimistic with rollback. | "Refusals are normal outcomes, not errors." |
| 25 to 35 | The ask flow: debounced similar-question search with cancellation, validation, local drafts. | "The cheapest duplicate is the one never posted." |
| 35 to 45 | SEO, 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.
Follow-ups to expect
Think first
"Members see neutral arrows for a moment before their votes appear. Can you remove that?"
First measure it: the viewer-state request starts in the head with a high fetch priority, so on a warm connection it often lands within a couple of hundred milliseconds (an estimate), before most readers look at the arrows.
If it still matters, render members' pages with an edge function that fetches viewer state and stitches it in, or skip the edge cache for signed-in members only. Both trade cache simplicity for one less flash. The anonymous path stays untouched either way.
Think first
"A question goes viral on social media while people are actively answering it. What breaks?"
Each new answer purges the page, so the edge keeps missing while traffic spikes. Fix it with request collapsing at the edge (one origin fetch for many waiting readers) and stale-while-revalidate, so readers get the previous version while one request refreshes it.
The socket fan-out grows too: thousands of readers subscribed to one question. Anonymous readers do not need the socket on most pages, and for those who do, batch score-changed events to at most one every few seconds.
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-staterequest 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, andQAPagestructured 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 inlocalStorage. - 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-pressedwith 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.
References
- Stack Overflow: The Architecture - 2016 Edition (Nick Craver) Servers, caching layers, and render times behind the site.
- Stack Overflow help center: Privileges The current reputation thresholds for every action.
- Stack Overflow help center: Vote down Downvote costs and daily vote limits.
- Google Search Central: Q&A (QAPage) structured data Required properties and content guidelines for question pages.
- Outdated Answers: accepted answer is now unpinned on Stack Overflow The 2021 announcement that made unpinning permanent, after the A/B test.
- Stack Overflow help center: What is reputation? Reputation gained and lost per vote, accept, and suggested edit, and the daily cap.
- A new look for comments (Stack Overflow blog) October 2025: commenting for all members, threaded replies, and comment votes.
- Moving the public Stack Overflow sites to the cloud: Part 1 How the sites left their own data centers in 2025.
- Goodbye, Prettify. Hello highlight.js! The 2020 switch of syntax highlighters.
- Introducing Staging Ground (Stack Overflow blog) Private review of new users' questions before they go public.
- MDN: Cache-Control
s-maxage,stale-while-revalidate,private, andno-store. - web.dev: Web Vitals LCP, INP, and CLS targets.
Concepts behind it
- Server-side renderingThe server builds the full HTML for each request, so users see content before any JavaScript runs, then the browser hydrates it: faster first paint and better SEO, paid for with server work, a later first byte, and a second render in the browser.
- CDN cachingKeeping shared copies of your responses on servers near every user, so most requests never travel to your origin: the biggest lever for global speed and for surviving traffic spikes.
- Islands and partial hydrationShip a page as static HTML and send JavaScript only for the few parts that need it, each waking up on its own trigger, so content pages stay fast on slow phones instead of paying to hydrate the whole app.
- Optimistic updatesShowing the result of a user's action at once, before the server confirms it, and quietly undoing it if the server says no: the trick that makes likes, checkboxes, and chat messages feel instant on any network.
- Debounce and throttleTwo small timing tools that turn a flood of events (keystrokes, scrolls, resizes) into a few calls you can afford: debounce waits for a pause, throttle runs at a steady rate.
- The URL as stateFilters, sort, page, tab, and selected item stored in the address bar instead of only in memory, so a view can be shared, bookmarked, refreshed, and walked back with the Back button: the cheapest state store the browser gives you.
- Cross-site scripting (XSS)Cross-site scripting is when text from outside your app ends up running as code inside your page, with every power your own code has: the most common and most damaging frontend security bug, and the one frontend engineers own most directly.
- WebSocketsOne long-lived connection, opened by an HTTP handshake, on which browser and server can each send a message at any moment. The usual pick for chat, multiplayer, and collaboration, and the one where you own the protocol, reconnects, and scaling.