CMD Guide
HomeOO & Low-Level DesignOO Design Problems

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:

EventEffect on target's reputationEffect on actor
Upvote on your answer+100
Upvote on your question+50
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 amount0 (offered up front)

And the privilege gates the score unlocks:

ReputationPrivilege unlocked
15Upvote
50Comment anywhere
125Downvote (costs the actor −1 on answers)
3000Vote to close / reopen
diagram
diagram

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.

#EventRule appliedDev's reputationPrivileges Dev can now use
0Account createdstart = 11ask, answer (none gated)
1Dev's question gets 3 upvotes3 × (+5)16+ upvote (crossed 15)
2Dev's answer gets 4 upvotes4 × (+10)56+ comment (crossed 50)
3Dev's answer is accepted by asker Ada+15 (Dev); Ada +271(no new gate)
4A troll downvotes Dev's answer−269(no change to gates)
5Bounty 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:

  1. 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 through Vote.castUpvote → ReputationService.award makes "a vote happened" a single transaction with one source of truth for the delta.
  2. Stored privileges go stale. If you cache boolean canComment on 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. Deriving can(Privilege) from the live score on each call makes that class of bug impossible.

Pitfalls a working engineer hits

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:

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


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.

🎨 Explain it visually

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.
🤔 Walk me through it (interactive)

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.
🧪 Quiz me & fix my gaps

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.
🧠 Make it stick

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.

📝 My notes