Design Stack Overflow
The whole site runs on one feedback loop: every vote, accepted answer, and bounty award is an event that adjusts a member's single integer reputation, and that integer is the gate that unlocks privileges (comment, vote, edit, close) and the trigger that mints badges. Model that event-to-reputation-to-privilege pipeline correctly and the rest of the design (questions, answers, tags, photos) is just data hanging off it.
The standard template for this problem stops at attribute bags — a Member with an int reputation field and a Bounty with a modifyReputation stub — and never shows who changes reputation, by how much, or how a privilege check reads it. That is the gap this page fills.
The reputation mechanism
Reputation is never written directly. A single component — call it the ReputationService — owns every rule for how points move, and it is the only code that mutates a member's score. Everything else (casting a vote, accepting an answer, awarding a bounty) calls into it. This matters because the same logical event ("answer got an upvote") must do three things atomically: change the author's score, possibly award a badge, and possibly cross a privilege threshold. If those rules are scattered across Answer.incrementVoteCount(), Question.addBounty(), and Member, they drift out of sync.
The canonical Stack Overflow point values:
| Event | Effect on target's reputation | Effect on actor |
|---|---|---|
| Upvote on your answer | +10 | 0 |
| Upvote on your question | +5 | 0 |
| Your answer is accepted | +15 | +2 (to the accepter) |
| Downvote on your post | −2 | −1 (only if downvoting an answer) |
| Bounty awarded to your answer | + full bounty amount | 0 (offered up front) |
And the privilege gates the score unlocks:
| Reputation | Privilege unlocked |
|---|---|
| 15 | Upvote |
| 50 | Comment anywhere |
| 125 | Downvote (costs the actor −1 on answers) |
| 3000 | Vote to close / reopen |
Worked trace: one member, four events
Member Dev starts a fresh account at reputation 1 (everyone starts at 1). Watch each event flow through ReputationService and re-derive her privileges. The accepted-answer rule also awards the accepter +2, so we track two members.
| # | Event | Rule applied | Dev's reputation | Privileges Dev can now use |
|---|---|---|---|---|
| 0 | Account created | start = 1 | 1 | ask, answer (none gated) |
| 1 | Dev's question gets 3 upvotes | 3 × (+5) | 16 | + upvote (crossed 15) |
| 2 | Dev's answer gets 4 upvotes | 4 × (+10) | 56 | + comment (crossed 50) |
| 3 | Dev's answer is accepted by asker Ada | +15 (Dev); Ada +2 | 71 | (no new gate) |
| 4 | A troll downvotes Dev's answer | −2 | 69 | (no change to gates) |
| 5 | Bounty is awarded to Dev's answer (Ada had placed a +50 bounty) | + full bounty (see note on direction) | 119 | (no new gate) |
Bounty direction matters and is the most common modelling mistake. The member who offers a bounty pays the reputation up front (it is deducted when the bounty is placed). The member whose answer the bounty is awarded to receives the full amount, and — unlike normal upvotes — bounty reputation is granted even on the bounty-giver's own question. So if Ada places a 50-rep bounty on Dev's answer and awards it, Ada was debited 50 at offer time and Dev gains +50 at award time. Net reputation in the system is conserved; the template's Bounty.modifyReputation() stub hides this two-sided flow entirely.
The classes, with the mechanism wired in
The key changes versus the attribute-bag template: (1) privileges are derived from reputation, not stored; (2) ReputationService is the single writer and applies named rules; (3) voting goes through it instead of a bare incrementVoteCount().
public enum ReputationRule {
QUESTION_UPVOTE(5),
ANSWER_UPVOTE(10),
ANSWER_ACCEPTED(15),
ACCEPTER_BONUS(2),
POST_DOWNVOTE(-2),
ANSWER_DOWNVOTE_ACTOR(-1);
private final int delta;
ReputationRule(int delta) { this.delta = delta; }
public int delta() { return delta; }
}
public enum Privilege {
UPVOTE(15), COMMENT(50), DOWNVOTE(125), CLOSE_REOPEN(3000);
private final int threshold;
Privilege(int t) { this.threshold = t; }
public int threshold() { return threshold; }
}
public class Member {
private final String id;
private int reputation = 1; // everyone starts at 1
private final List<Badge> badges = new ArrayList<>();
// package-private: ONLY ReputationService may move the score
void applyDelta(int delta) {
this.reputation = Math.max(1, this.reputation + delta); // floor at 1
}
public int getReputation() { return reputation; }
void addBadge(Badge b) { badges.add(b); }
// privileges are DERIVED, never stored — cannot drift from the score
public boolean can(Privilege p) {
return reputation >= p.threshold();
}
}// The single writer. Every reputation change in the whole system goes here,
// so the badge check and privilege check always see one consistent value.
public class ReputationService {
private final List<ReputationObserver> observers;
public ReputationService(List<ReputationObserver> observers) {
this.observers = observers; // e.g. BadgeService, NotificationService
}
public void award(Member target, ReputationRule rule) {
int before = target.getReputation();
target.applyDelta(rule.delta());
int after = target.getReputation();
// fan out: badges minted, notifications sent, privilege crossings logged
for (ReputationObserver o : observers) {
o.onReputationChanged(target, before, after);
}
}
}
public interface ReputationObserver {
void onReputationChanged(Member m, int before, int after);
}
public class BadgeService implements ReputationObserver {
@Override
public void onReputationChanged(Member m, int before, int after) {
// 'Pundit' badge at 200, etc. Idempotent: only fires on the crossing.
if (before < 200 && after >= 200) {
m.addBadge(new Badge("Pundit", "Reached 200 reputation"));
}
}
}public class Vote {
// Casting a vote is what triggers reputation — not a bare counter bump.
public static void castUpvote(Post post, Member voter, ReputationService rep) {
if (!voter.can(Privilege.UPVOTE)) {
throw new InsufficientPrivilegeException("need 15 rep to upvote");
}
post.recordVote(+1); // display counter on the post
rep.award(post.getAuthor(), // the author gains reputation
post instanceof Answer ? ReputationRule.ANSWER_UPVOTE
: ReputationRule.QUESTION_UPVOTE);
}
}
public abstract class Post {
protected int voteCount;
protected Member author;
void recordVote(int delta) { this.voteCount += delta; }
public Member getAuthor() { return author; }
}
public class Answer extends Post {
private boolean accepted;
public void accept(Member asker, ReputationService rep) {
if (accepted) return;
accepted = true;
rep.award(this.author, ReputationRule.ANSWER_ACCEPTED); // +15
rep.award(asker, ReputationRule.ACCEPTER_BONUS); // +2
}
}
public class Question extends Post {
private QuestionStatus status = QuestionStatus.OPEN;
private final List<Answer> answers = new ArrayList<>();
private final List<Tag> tags = new ArrayList<>();
private Bounty bounty; // nullable
}Why the naive version is wrong
The template's design has each post mutate its own state with incrementVoteCount() and stores reputation as a plain settable field. Two concrete bugs fall out of that:
- Display count and earned reputation diverge. If
Answer.incrementVoteCount()bumps the post's counter but the +10 to the author's reputation is added by separate caller code, a refactor that adds a second call site (mobile API, bulk import, a moderator re-tally) will increment one without the other. Routing both throughVote.castUpvote → ReputationService.awardmakes "a vote happened" a single transaction with one source of truth for the delta. - Stored privileges go stale. If you cache
boolean canCommenton the member, you must remember to recompute it on every reputation change — and the troll-downvote at step 4 above can push someone back below a threshold. Derivingcan(Privilege)from the live score on each call makes that class of bug impossible.
Pitfalls a working engineer hits
- Self-votes and vote-rescinding. A member upvoting their own post must be rejected; un-voting must apply the exact inverse delta. If the inverse isn't symmetric (e.g. you granted +10 on upvote but only claw back +5 on rescind), reputation inflates. Keep the delta in
ReputationRuleso the inverse is literally-rule.delta(). - Reputation floor. Downvotes can never push a member below 1. The naive
reputation += deltalets a flood of downvotes go negative; theMath.max(1, …)floor inapplyDeltais the standard rule — and means a downvote at reputation 1 is a no-op you must not double-charge for. - Daily reputation cap. Real Stack Overflow caps vote-driven reputation gain at +200/day (bounties and accepts are exempt). If you don't model this, a brigaded answer can mint unlimited rep. The cap belongs in
ReputationService, the single writer — not sprinkled across posts. - Badge double-minting. An observer that fires whenever
after >= 200will re-mint the badge on every subsequent gain. Gate on the crossing (before < 200 && after >= 200), which is whyonReputationChangedpasses both values. - Bounty escrow. Bounty rep is deducted from the offerer when the bounty is placed, not when awarded — otherwise a member could offer bounties they can't pay and award them anyway. Model the offer as a debit into escrow.
Selection & trade-offs: Observer vs. direct calls vs. polling
The load-bearing design decision is how reputation changes propagate to badges, notifications, and privilege re-checks. Three options:
- Observer (chosen):
ReputationServiceholds a list ofReputationObservers and fans out after each change. Gain: adding a new reaction (a new badge tier, an analytics sink) is a new observer with zero edits to the voting code — open/closed. Cost: indirection (you can't read the call site and see every consequence), and observers run synchronously in the vote's critical path unless you push them onto a queue. - Direct method calls:
ReputationServicecallsbadgeService.check(...)andnotifier.send(...)inline. Gain: dead simple, fully traceable. Cost:ReputationServicenow depends on and must be edited for every new consumer — exactly the coupling that bit the template. - Polling / batch recompute: a nightly job re-derives everyone's badges and reputation from the raw vote log. Gain: the write path stays trivial; you can re-run rules after a rule change. Cost: badges and privileges lag by up to a day, and recomputing all members doesn't scale.
Choose Observer when reactions to a reputation change are open-ended and you want the vote path closed to modification (the SO case — badges, notifications, anti-abuse all want the same event). Prefer direct calls when there are exactly one or two fixed consumers and traceability beats extensibility. Prefer batch recompute when rules change often and eventual consistency on badges is acceptable — which is in fact how real Stack Overflow recalculates reputation after large rule changes, running both: live Observer for the common path, batch for corrections.
Takeaways
- Reputation is the system's spine: one integer, written by exactly one component, read everywhere. Model that writer first; questions and answers are data around it.
- Derive privileges from the live score — never store them — and gate badges on the crossing so they can't double-mint or go stale.
- Keep each rule's delta in one place so rescinding a vote is the exact inverse; add the daily cap and the floor-at-1 in the single writer.
- The Observer pattern decouples "reputation changed" from its consequences; reach for direct calls only when consumers are few and fixed.
Re-authored and deepened for this guide. Reputation point values, privilege thresholds, the +200/day cap, and bounty escrow mechanics follow Stack Overflow's published "What is reputation?" and privileges and bounty help pages. The class skeleton derives from the Grokking the Object Oriented Design Interview "Design Stack Overflow" template, restructured around a single-writer ReputationService and the Observer pattern (Gamma et al., Design Patterns). The original template page's reputation/badge/bounty mechanics, traced example, and Observer-vs-alternatives trade-off were added here.
🤖 Don't fully get this? Learn it with Claude
Stuck on Design Stack Overflow? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **Design Stack Overflow** (OO & Low-Level Design) and want to truly understand it. Explain Design Stack Overflow from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **Design Stack Overflow** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **Design Stack Overflow** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **Design Stack Overflow** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.