Skip to main content

Stack Overflow

A Q&A site: anyone can ask, anyone can answer, and a number next to your name keeps score. The interesting part isn't the CRUD - questions and answers look almost identical on paper - it's noticing that they are almost identical, and building one shape for both instead of two.

Requirements​

Functional

  • A user posts a question with a title, a body, and a set of tags.
  • Other users post answers to that question; the original asker can mark exactly one answer accepted.
  • Users upvote or downvote a question or an answer; a user's vote can change but never stacks (one user, one live vote per post).
  • Users comment on questions and answers.
  • Questions can be looked up by tag.

Non-functional

  • Looking up questions by tag must not scan every question in the system - the index should do the work, not a filter.
  • The reputation formula is tuned constantly (an upvote is worth more some months than others, an accepted answer even more) and that tuning should never touch User or Question.

Design​

Question and Answer both carry votes and comments and both belong to a user - the only real difference is that a question also has a title and tags. Duplicating vote-tally and comment-list logic in two classes would mean every future bug gets fixed once and re-introduced once, so both extend a shared Post that owns that behavior and nothing else.

UserQaPlatformQuestionAnswerReputationPolicyaskQuestion(title, body, tags)1new Question(...)2postAnswer(question, body)3new Answer(...)4vote(answer, +1)5applyVote(userId, +1)6onVote(answer, delta)7acceptAnswer(question, answer)8
  1. 1Asking and answering both go through the same front door.
  2. 2QaPlatform creates the post and indexes it by tag; it never hands the caller a raw map to mutate.
  3. 3A second user answers - same platform method family, a sibling post type.
  4. 4Answer and Question are siblings under Post, so this call looks identical from the platform side.
  5. 5Voting is a single method regardless of whether the target is a question or an answer.
  6. 6The post reports back a delta, not a raw new total - it doesn’t know what that delta is worth.
  7. 7The policy is the only class that turns a delta into reputation points.
  8. 8Only the asker can call this; it flips the question’s accepted-answer pointer and scores the answerer.

Reputation never gets touched directly by Post - every point awarded or docked runs through a ReputationPolicy, so retuning the scoring is a one-file change.

Class diagram​

«create»«create»«abstract»Post-author: User-votes: Map<userId, int>-comments: List<Comment>+applyVote(userId, value): int+addComment(c)+score(): int«interface»ReputationPolicy+onVote(post, delta)+onAnswerAccepted(answer)QaPlatform-tagIndex: TagIndex-reputationPolicy: ReputationPolicy+askQuestion(u, title, body, tags): Question+postAnswer(u, q, body): Answer+vote(u, post, value)+acceptAnswer(asker, q, a)Question-title: string-tags: Set<string>-answers: List<Answer>-acceptedAnswerId: string+accept(answerId)Answer-questionId: stringComment-author: User-text: stringUser-id: string-name: string-reputation: intTagIndex+indexQuestion(q)+findByTag(tag): Set<Question>DefaultReputationPolicy+onVote(post, delta)+onAnswerAccepted(answer)
extendsimplementsassociationdependency
Question and Answer share Post's vote/comment plumbing; ReputationPolicy is the only thing that knows what a vote is worth.

Code​

import java.util.*;
 
class User {
final String id;
final String name;
int reputation = 0;
 
User(String id, String name) {
this.id = id;
this.name = name;
}
}
 
class Comment {
final User author;
final String text;
 
Comment(User author, String text) {
this.author = author;
this.text = text;
}
}
 
abstract class Post {
final String id;
final User author;
private final Map<String, Integer> votes = new HashMap<>();
private final List<Comment> comments = new ArrayList<>();
 
Post(String id, User author) {
this.id = id;
this.author = author;
}
 
// Returns the change in score caused by this vote, so the caller can
// decide what that change is worth without Post knowing about points.
int applyVote(String userId, int value) {
int previous = votes.getOrDefault(userId, 0);
if (value == 0) votes.remove(userId);
else votes.put(userId, value);
return value - previous;
}
 
void addComment(Comment c) {
comments.add(c);
}
 
int score() {
return votes.values().stream().mapToInt(Integer::intValue).sum();
}
}
 
class Question extends Post {
final String title;
final Set<String> tags;
final List<Answer> answers = new ArrayList<>();
String acceptedAnswerId;
 
Question(String id, User author, String title, Set<String> tags) {
super(id, author);
this.title = title;
this.tags = tags;
}
 
void accept(String answerId) {
this.acceptedAnswerId = answerId;
}
}
 
class Answer extends Post {
final String questionId;
 
Answer(String id, User author, String questionId) {
super(id, author);
this.questionId = questionId;
}
}
 
class TagIndex {
private final Map<String, Set<Question>> byTag = new HashMap<>();
 
void indexQuestion(Question q) {
for (String tag : q.tags) {
byTag.computeIfAbsent(tag, t -> new HashSet<>()).add(q);
}
}
 
Set<Question> findByTag(String tag) {
return byTag.getOrDefault(tag, Collections.emptySet());
}
}
 
interface ReputationPolicy {
void onVote(Post post, int delta);
void onAnswerAccepted(Answer answer);
}
 
class DefaultReputationPolicy implements ReputationPolicy {
private static final int POINTS_PER_VOTE = 10;
private static final int ACCEPTED_BONUS = 15;
 
public void onVote(Post post, int delta) {
post.author.reputation += delta * POINTS_PER_VOTE;
}
 
public void onAnswerAccepted(Answer answer) {
answer.author.reputation += ACCEPTED_BONUS;
}
}
 
class QaPlatform {
private final TagIndex tagIndex = new TagIndex();
private final ReputationPolicy reputationPolicy;
private final Map<String, Question> questions = new HashMap<>();
private int nextId = 1;
 
QaPlatform(ReputationPolicy reputationPolicy) {
this.reputationPolicy = reputationPolicy;
}
 
Question askQuestion(User author, String title, String body, Set<String> tags) {
Question q = new Question(String.valueOf(nextId++), author, title, tags);
questions.put(q.id, q);
tagIndex.indexQuestion(q);
return q;
}
 
Answer postAnswer(User author, Question question, String body) {
Answer answer = new Answer(String.valueOf(nextId++), author, question.id);
question.answers.add(answer);
return answer;
}
 
void vote(User voter, Post post, int value) {
int delta = post.applyVote(voter.id, value);
reputationPolicy.onVote(post, delta);
}
 
void acceptAnswer(User asker, Question question, Answer answer) {
if (!question.author.id.equals(asker.id)) {
throw new IllegalStateException("Only the asker can accept an answer");
}
question.accept(answer.id);
reputationPolicy.onAnswerAccepted(answer);
}
}

Design decisions​

  • Post is an abstract base for Question and Answer, not two independent classes. Voting, vote-changing, and commenting are identical for both; only the tagging and accepted-answer bookkeeping are question-specific. Pulling the shared half into Post means a vote-tallying bug gets fixed in one place, not found twice.
  • A vote is a map entry keyed by user, not a growing list. applyVote looks up the caller's previous value, replaces it, and returns the delta. That's what makes "change your vote" and "vote once" the same code path instead of a special case bolted onto a running counter.
  • Reputation scoring lives entirely behind ReputationPolicy. Post.applyVote reports a delta; it has no idea whether that delta is worth 5 points or 10, or whether an accepted answer is worth more than an upvote. That knowledge sits in exactly one place, so tuning the curve never means re-reading Post or QaPlatform.
  • TagIndex is a plain map from tag to question set, updated on write. findByTag never touches a question it doesn't need to - the cost of indexing is paid once, at ask-time, instead of on every lookup.
  • What's missing for a real system: duplicate-question detection (near-duplicate title matching), comment threading beyond a flat list, and a moderation/close-vote workflow are all real Stack Overflow features left out here because none of them change the shape above - they'd each be a new class hanging off Post or Question, not a rewrite of it.

Common follow-ups​

  • How would you add comment voting later? Give Comment the same vote-map behavior Post has, either via a shared Votable interface/mixin both Post and Comment implement - a sign that extracting the vote logic out of Post entirely, rather than inheriting from it, would be the cleaner move at that point.
  • How do you prevent someone from upvoting their own answer? QaPlatform.vote gains a check comparing voter.id to post.author.id before calling applyVote - a one-line guard in the platform's single vote() method, not something Post needs to know about.
  • How would you support bounties (extra reputation for a good answer)? A bounty is naturally another ReputationPolicy concern - a new onBountyAwarded hook just adds more points, since all point-awarding already funnels through one policy object.
  • How do you find the best answer without scanning every answer on a question? Question already tracks acceptedAnswerId; a "best answer other than accepted" would need a maintained top-voted-answer id updated at vote time, mirroring how TagIndex avoids scanning for tag lookups.

Check yourself

Question 1 of 4

Why do Question and Answer both extend Post instead of being two unrelated classes?