Code a Q&A site with votes and reputation
Open in the interactive app: practice, quiz, mock interview →In one minute
- The hard part is not storing questions and answers; it is the rules engine around them. Votes, accepted answers and reputation interact, and every one of them can be changed or undone later.
- Store one vote per (voter, post) as a direction: up, down or none. A vote change is then "remove the old effect, add the new one", so undo and up-to-down switches are exact and cannot drift.
- Reputation is a projection: 1 plus the sum of the effects of every vote and acceptance that currently stands. Apply it incrementally for speed, but keep the inputs so it can always be recomputed and checked.
- Privileges are thresholds on reputation (upvote at 15, comment anywhere at 50, downvote at 125). Put the numbers in one policy object, not scattered
if rep >= 125checks. - The problem shape is contention on hot rows (a viral answer's score and its author's reputation) plus trust but verify (recompute reputation from the vote log to catch drift).
Requirements
Functional
- Users ask questions, post answers to questions, and comment on either.
- Users vote a post up or down, change their vote, or withdraw it. Nobody votes on their own post.
- Reputation rules for this design: an upvote gives the post's author +10; a downvote costs the author 2 and the voter 1. Accepting an answer gives its author +15 and the asker +2, unless the asker accepted their own answer.
- The asker can accept one answer, switch to another, or unaccept.
- Privileges by reputation: upvote at 15, comment on other people's posts at 50, downvote at 125. Anyone can comment on their own posts.
- Show each post's score and each user's reputation.
Non-functional
- Reputation and scores are exact and explainable: any user's number can be rebuilt from the votes that stand.
- Concurrent votes on a popular post never lose an update.
- Reads vastly outnumber writes, so page views compute nothing expensive.
Out of scope
- Search, tags, badges, bounties, edits with revision history, moderation queues and close votes. We note where each plugs in.
Entities and relationships
User has an id and a reputation. Post is the shared base for Question and Answer: an id, an author, a body, a score and a list of Comment objects. A Question also has a title, its answer ids and an optional accepted answer id. An Answer points to its question.
Vote is the relationship between a user and a post with a direction. It is keyed by (post, voter), which makes "one vote per user per post" a property of the data structure rather than a rule someone must remember to check. ReputationPolicy maps events (upvote received, downvote received, downvote cast, answer accepted, accepted an answer) to point values and holds the privilege thresholds. QAService is the facade that validates a request, consults the policy, and updates posts, votes and reputations together.
Comments carry no votes or reputation here, so they are a value list on a post rather than a third kind of Post.
Class diagram
classDiagram
class User {
+id
+reputation int
}
class Post {
<<abstract>>
+id int
+author
+score int
+comments list~Comment~
}
class Question {
+title
+answerIds list~int~
+acceptedId int
}
class Answer {
+questionId int
}
class Comment {
+id int
+author
+text
}
class Vote {
+postId int
+voter
+direction
}
class ReputationPolicy {
+voteEffect(direction) Effect
+acceptEffect() Effect
+threshold(privilege) int
}
class QAService {
+ask(user, title) int
+answer(user, questionId, body) int
+comment(user, postId, text) int
+vote(user, postId, direction)
+accept(user, answerId)
+unaccept(user, questionId)
}
Post <|-- Question
Post <|-- Answer
Post "1" *-- "*" Comment
Question "1" o-- "*" Answer
Vote --> Post
Vote --> User
QAService --> ReputationPolicy
QAService --> Post
QAService --> UserKey flows
The interesting flow is a vote change. Cat upvoted Bob's answer yesterday and now switches to a downvote. The service reads the stored direction, checks the privilege for the new direction, then reverses the old effect and applies the new one in one critical section.
sequenceDiagram
participant C as Cat
participant S as QAService
participant V as VoteStore
participant P as ReputationPolicy
participant R as Reputation
C->>S: vote(cat, answer 2, down)
S->>V: get(answer 2, cat)
V-->>S: up
S->>P: threshold(downvote)
P-->>S: 125
S->>S: check cat >= 125 and cat is not author
S->>R: undo up: bob -10, score -1
S->>R: apply down: bob -2, cat -1, score -1
S->>V: put(answer 2, cat, down)
S-->>C: okAccepting works the same way: reverse the previous acceptance's effect, then apply the new one.
Design patterns used and why
Strategy (a policy object) for reputation and privileges. Point values have changed over the site's life: question upvotes went from +5 to +10 in 2019, and the real site charges the voter only for downvoting answers, not questions. When the numbers live in one ReputationPolicy, a rule change is a one-file diff plus a backfill job, and tests can run with a toy policy.
Inheritance for Post, used sparingly. Questions and answers really do share identity, authorship, score, voting and comments, and voting code should not care which one it got. The subclasses add only data (title, accepted id, question link). The "is-a" relation genuinely holds here.
Observer or domain events for side effects. A VoteChanged(post, voter, old, new) event is what badges, notifications, the "hot questions" ranker and the search indexer subscribe to. The practice applies reputation inline; at scale it becomes one more subscriber.
Facade in QAService: one method per user action, so the controller layer never coordinates posts, votes and reputations itself.
Concurrency and edge cases
Exactly one vote per user per post
Two clicks from the same user arrive at once (a double tap, or a retry after a timeout). With the vote keyed by (post, voter) and the read-compare-write done under one lock, the second request sees the direction already stored and is a no-op. In a database, a primary key on (post_id, user_id) plus INSERT ... ON CONFLICT (post_id, user_id) DO UPDATE gives the same guarantee, and the delta logic runs in the same transaction.
Hot rows
A viral answer receives hundreds of votes a minute, and every one updates the same score row and the same author's reputation row. Row locks serialise them; that is correct but makes latency grow with popularity. Keep the transaction tiny (one insert, two increments), always lock in the same order (post, then author, then voter) so two transactions cannot deadlock, and use score = score + delta instead of read-then-write. If still too hot, log votes and fold them into the score asynchronously.
Privilege checks use current reputation
Privileges gate new actions; a user who falls below 125 keeps the downvotes already cast. Withdrawing a vote never needs a privilege, otherwise a user who lost reputation could be stuck with a vote they regret.
Accepting
Only the asker can accept, only an answer to that question can be accepted, and accepting the already accepted answer is a no-op. Self-accepts are allowed but earn nothing, which closes the obvious farming loop of answering your own question and accepting it.
Caps and floors are policy
The real site floors reputation at 1 and caps vote reputation at 200 per day. Both break the "sum of effects" model (with a floor, undoing a downvote can return more than was taken), so they need the event log recomputed in time order with an injected clock.
Down the stack
Layer 1The contract
For every (post, voter) there is at most one standing vote. A post's score equals ups minus downs among standing votes. Every user's reputation equals 1 plus the sum of the policy effects of all standing votes and acceptances. Any sequence of vote changes that ends in the starting state leaves every number exactly where it began. A rejected action changes nothing.
Layer 2The mechanism
In memory: a map from post id to post, a map from user to reputation, and per post a map from voter to direction. A vote is O(1): one lookup, a delta of at most four integer updates, one store. The delta is effect(new) - effect(old) with effect(none) = 0, which is why undo and switch need no special cases. In a relational store this maps directly to the public Stack Exchange data dump's shape: a Votes table with a VoteTypeId (1 for accepted by originator, 2 for up, 3 for down), a denormalised Score on Posts, and Reputation on Users. Denormalised totals make the page read cheap; the votes table makes them verifiable.
Layer 3The machine
A vote in Postgres or SQL Server is a short transaction: insert or update a vote row of a few dozen bytes, update two rows with an increment, write the redo log and fsync on commit. On NVMe that commit is tens of microseconds; on networked storage it is about a millisecond, so one hot row tops out around a thousand serialised votes per second before batching helps. In memory, an uncontended Go sync.Mutex costs about 20 ns and the whole vote under it well under a microsecond. Stack Overflow's published 2016 architecture ran on a small fleet of SQL Server and web machines, which works because reads hit caches and writes like votes are tiny transactions.
Why it's built this way
People change their minds, so votes are stored as current state per (post, voter), not as counters. Reputation must be explainable when someone asks "why did I lose 2 points?", so it is a projection of standing votes. Rules change over the years, so the numbers sit in one policy object. Popular posts concentrate writes on one row, so the transaction is minimal and ordered. Reads dominate, so totals are denormalised and the log exists to verify them.
Extensibility
- Bounties: a new event type that moves reputation from the sponsor to the awarded answer; one policy entry and one service method, plus an expiry with an injected clock.
- Badges and notifications: subscribers to
VoteChangedandAnswerAcceptedevents; the voting code does not change. - Close and delete: a State for questions (open, closed, deleted). Deleting a post reverses the reputation from its votes, which the projection model handles by excluding it from the sum.
Leadership lens
- The first review question is "can we rebuild every user's reputation from scratch, and does it match?" Run that recomputation in CI on a fixture and nightly in production; it catches every class of drift bug.
- Rule changes are product decisions with data migrations attached. The 2019 change to question upvote value needed a recalculation of every user's reputation; plan the backfill, the user communication and the rollback before merging the policy diff.
- Hot-row contention is a capacity question for the database owner, not just the feature team. Agree on a threshold (for example, votes per second on one post) at which the team switches to asynchronous score folding.
- Keep anti-abuse (serial voting, sock puppets) in a separate service that reverses votes through the normal
vote(..., none)path, so the core stays simple and reputation stays consistent.
Check yourself
Why is a vote stored as a direction per (post, voter) instead of incrementing
upsanddownscounters?Show answer
Counters cannot tell whether this user already voted, so they cannot enforce one vote per user, reverse a vote exactly, or rebuild reputation. The per-voter record makes undo a delta between two known states.
Cat switches her vote on Bob's answer from up to down. What happens to Bob's and Cat's reputation and to the score?
Show answer
Bob loses the 10 from the upvote and 2 more for the downvote (-12), Cat pays 1, and the score drops by 2. Undoing it later restores all three exactly.
Ann accepted Bob's answer, then switches to Dan's. Which numbers change?
Show answer
Bob loses 15, Dan gains 15, and Ann's +2 stays because she still has an accepted answer. If Dan's answer were Ann's own, Ann would lose the 2 and nobody would gain.
A user at 125 downvotes and drops to 124. Can they withdraw that downvote? Should they be able to?
Show answer
Yes. Withdrawing needs no privilege; otherwise users who lose reputation would be stuck with votes they cannot change.
Watch
- Low Level Design: Live Coding, Making StackOverflow (Utkarsh Bindal): a live-coded object model for questions, answers and votes.
- Flipkart LLD Round: Stack Overflow Clone, Part 1 (Deep Tech): the problem as asked in a machine-coding round, with requirements and classes.
- Stack Overflow Low Level Design (Soumyajit Bhattacharyay): a full code walkthrough with SOLID-based class design.
Free resources
- Stack Overflow: The Architecture, 2016 Edition (Nick Craver): how the real site serves enormous read traffic from a small number of servers.
- We're rewarding the question askers (Stack Overflow blog): the 2019 change of question upvotes from +5 to +10, a real example of a reputation rule change.
- Stack Exchange data dump (Internet Archive): the public Posts, Votes and Users tables, useful to see how votes and scores are actually stored.
- Event Sourcing (Martin Fowler): the idea behind treating reputation as a projection of a vote log.
- INSERT and ON CONFLICT (PostgreSQL docs): the upsert that enforces one vote per user per post in a database.
- Observer pattern (Refactoring.Guru): the pattern behind badges, notifications and indexers reacting to vote events.