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

«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>- 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)
implementsextendsusescreates
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;
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) {
return new Answer(String.valueOf(nextId++), author, question.id);
}
 
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.
0%0 of 122 pages studied