Problem Statement
Design a voting and reaction system that allows users to upvote, downvote, or attach emoji reactions to content items such as posts, comments, or answers. The system must handle high concurrency, prevent duplicate votes, support real-time vote count updates, and include anti-fraud throttling.
Clarifying Questions
- Do we support multiple reaction types (like, love, laugh) or just binary up/down votes?
- Can a user change or retract their vote?
- Do vote counts need to be strongly consistent or is eventual consistency acceptable?
- What is the expected read-to-write ratio? (Typically reads far outnumber writes.)
- Should vote counts be visible in real time (WebSocket/SSE) or on page refresh?
- Do we need per-user vote history for display (e.g., highlight the button the user already clicked)?
Core Requirements
Functional
- Cast, change, or retract a vote or reaction on any content item.
- Retrieve the current vote tally and the requesting user's vote state for a given item.
- Support a configurable set of reaction types (thumbs-up, heart, laugh, etc.).
- Broadcast count updates in near-real-time to subscribed clients.
Non-Functional
- Idempotent vote operations: clicking the same vote twice toggles it off, never double-counts.
- Throughput: support tens of thousands of vote writes per second at peak.
- Read latency: vote counts served in < 10 ms from cache.
- Anti-fraud: rate-limit vote casting per user and detect coordinated manipulation.
Data Model
votes table (MySQL / PostgreSQL)
CREATE TABLE votes ( user_id BIGINT NOT NULL, item_id BIGINT NOT NULL, item_type VARCHAR(32) NOT NULL, -- post, comment, answer vote_value SMALLINT NOT NULL, -- +1, -1, or 0 (retracted) reaction VARCHAR(32) DEFAULT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (user_id, item_id, item_type) );
The composite primary key (user_id, item_id, item_type) enforces one row per user-item pair, making duplicate-vote prevention a database-level guarantee rather than application logic.
vote_counts table (denormalized cache)
CREATE TABLE vote_counts ( item_id BIGINT NOT NULL, item_type VARCHAR(32) NOT NULL, upvotes BIGINT NOT NULL DEFAULT 0, downvotes BIGINT NOT NULL DEFAULT 0, PRIMARY KEY (item_id, item_type) );
reaction_registry table
CREATE TABLE reaction_types ( code VARCHAR(32) PRIMARY KEY, label VARCHAR(64) NOT NULL, icon_url TEXT, is_active TINYINT(1) NOT NULL DEFAULT 1 );
Storing reaction types in a table rather than an enum allows product teams to add or retire reactions without schema migrations.
API Design
POST /v1/votes
Body: { item_id, item_type, vote_value, reaction? }
Returns: { item_id, new_score, user_vote }
GET /v1/votes/{item_type}/{item_id}
Returns: { upvotes, downvotes, score, user_vote, reactions: { [code]: count } }
GET /v1/reactions
Returns: [ { code, label, icon_url } ]
Idempotent Vote Toggling
The core write operation uses an upsert with delta arithmetic to avoid race conditions:
-- Upsert the user vote row
INSERT INTO votes (user_id, item_id, item_type, vote_value)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
vote_value = VALUES(vote_value),
updated_at = NOW();
-- Recompute delta from old and new values, then adjust the counter
UPDATE vote_counts
SET upvotes = upvotes + GREATEST(0, new_val) - GREATEST(0, old_val),
downvotes = downvotes + GREATEST(0, -new_val) - GREATEST(0, -old_val)
WHERE item_id = ? AND item_type = ?;
Both statements execute inside a single transaction. Because the primary key prevents two rows for the same user-item pair, re-submitting the same vote is a no-op at the database level.
Real-Time Vote Count Updates
After a successful write the application layer publishes a lightweight event to a Pub/Sub channel (e.g., Redis PUBLISH votes:{item_type}:{item_id} {score}). A WebSocket gateway subscribes to these channels and fans out the new count to all connected clients viewing that item. For clients that cannot maintain a persistent connection, a short-poll endpoint returning ETags keeps bandwidth minimal.
Because real-time delivery is best-effort, the authoritative count always comes from the database (or its cache). Clients reconcile on reconnect.
Caching Strategy
- Vote counts: cached in Redis with key
vc:{item_type}:{item_id}. TTL 60 seconds; invalidated on every write. - User vote state: cached per user per item with key
uv:{user_id}:{item_type}:{item_id}. TTL 5 minutes. - Reaction registry: cached globally for 1 hour; changes rarely.
- On a cache miss the application reads from a read replica, not the primary, to isolate read load.
Anti-Fraud Throttling
Rate Limiting
A sliding-window counter in Redis enforces a per-user vote budget:
Key: rl:vote:{user_id}
Type: Redis sorted set (score = timestamp, member = vote UUID)
Window: 60 seconds, limit: 60 votes
Before processing a vote the service calls ZREMRANGEBYSCORE to drop expired entries, then ZCARD to check the current count. If the limit is exceeded the request is rejected with HTTP 429.
Coordinated Manipulation Detection
- A background job aggregates votes per item over 5-minute windows. Items receiving more than 3 standard deviations above the rolling mean are flagged for manual review.
- Votes from accounts created within the last 24 hours are weighted at 0.1 in score calculations until the account ages.
- IP-level clustering: more than 10 unique user votes from the same /24 subnet within 60 seconds triggers a CAPTCHA challenge.
Scalability Considerations
- Sharding: partition the
votestable byitem_id % Nso that hot items are distributed across shards. - Write batching: for extremely viral items, buffer writes in a Kafka topic and apply them in micro-batches to
vote_counts, trading a few seconds of staleness for dramatically lower write pressure on the database. - Counter drift repair: a nightly job recomputes all
vote_countsfrom the rawvotestable to correct any drift introduced by failed transactions.
Edge Cases
- Concurrent same-direction votes: handled by the upsert; only one row exists per user-item pair.
- Vote on deleted item: a foreign-key constraint or a pre-write existence check prevents orphaned vote rows.
- Reaction type retired mid-flight: the API validates reaction codes against the registry before inserting; retired codes return 400.
- Network partition during write: idempotency keys on the POST endpoint allow clients to safely retry without double-counting.
Summary
A robust voting system anchors idempotency in the database schema through a composite primary key, uses delta-based counter updates inside transactions, pushes real-time counts through Redis Pub/Sub, and layers rate limiting and statistical anomaly detection to suppress fraud. Caching vote counts at the read path keeps p99 read latency under 10 ms even at millions of items.
See also: Meta Interview Guide 2026: Facebook, Instagram, WhatsApp Engineering
See also: Scale AI Interview Guide 2026: Data Infrastructure, RLHF Pipelines, and ML Engineering