What Is a User Follow Service?
A user follow (or subscribe) service manages directed relationships between users: when Alice follows Bob, Alice sees Bob’s content in her feed, and Bob’s follower count increments. This component sits at the center of every social network, creator platform, and notification system.
Requirements Clarification
Functional
- Follow and unfollow a user.
- Check whether user A follows user B (is-following query).
- Retrieve the list of users that a given user follows (following list).
- Retrieve the list of users that follow a given user (follower list).
- Return accurate follower and following counts.
- Detect mutual follows (friends).
- Optionally support follow requests (private accounts must approve before the follow is active).
- Provide follow recommendations.
Non-Functional
- 10,000 follow/unfollow writes per second.
- 100,000 is-following reads per second.
- Follower/following lists may be paginated; support up to millions of followers per celebrity account.
- Eventual consistency acceptable for counts; strong consistency required for is-following checks.
Data Model
Table: follows
follower_id BIGINT NOT NULL
followee_id BIGINT NOT NULL
created_at DATETIME NOT NULL
status ENUM('active','pending','blocked') NOT NULL DEFAULT 'active'
PRIMARY KEY (follower_id, followee_id)
INDEX (followee_id, follower_id) -- for follower list lookups
INDEX (follower_id, created_at) -- for following list sorted by time
The composite primary key (follower_id, followee_id) enforces uniqueness and enables O(1) is-following lookups. The secondary index on (followee_id, follower_id) is the inverse direction needed for follower-list queries.
Core Operations
Follow
- Validate that neither user is blocked by the other.
- For public accounts: INSERT INTO follows (follower_id, followee_id, status) VALUES (A, B, 'active') ON DUPLICATE KEY UPDATE status='active'.
- For private accounts: insert with status='pending'; send a follow-request notification to the followee.
- Increment follower count for B and following count for A in a counters table (or Redis).
- Emit a follow event to the event bus for downstream consumers (feed service, notification service, recommendation engine).
Unfollow
- DELETE FROM follows WHERE follower_id=A AND followee_id=B.
- Decrement counters atomically.
- Emit an unfollow event.
Is-Following
SELECT 1 FROM follows WHERE follower_id=A AND followee_id=B AND status='active' LIMIT 1. This hits the primary key index and is extremely fast. For even lower latency, cache the result in Redis with a short TTL (e.g., 10 seconds) or use a Bloom filter to eliminate negative lookups.
Follower and Following Counts
Counting rows on every request is too slow for high-traffic accounts. Use a dedicated counters table:
Table: user_follow_counts user_id BIGINT PRIMARY KEY follower_count BIGINT NOT NULL DEFAULT 0 following_count BIGINT NOT NULL DEFAULT 0 updated_at DATETIME
Update atomically using UPDATE user_follow_counts SET follower_count = follower_count + 1 WHERE user_id = B. For celebrity accounts receiving thousands of follows per second, batch these increments: buffer in Redis with INCR and flush to the DB periodically.
Mutual Follow Detection
A mutual follow (friendship) exists when both (A follows B) and (B follows A) are active rows.
- On-the-fly: query is-following in both directions. Two indexed point lookups; acceptable for most cases.
- Materialized: maintain a separate friends table with the canonical pair stored as (min(A,B), max(A,B)). Insert a row when both directions exist; delete when either direction is removed. Enables efficient friend-list queries without a self-join.
For the materialized approach, use a transaction or an event-driven reconciler: on each follow write, check if the reverse follow exists and update the friends table accordingly.
Celebrity / High-Fan-Out Handling
A celebrity with 50 million followers creates write amplification challenges for the feed service, not directly for the follow service. But the follow service still needs to handle:
- Hot partition problem: all follow writes for a celebrity land on the same database shard (keyed by followee_id). Mitigate by sharding the follows table by follower_id for write distribution, maintaining a separate followee-indexed replica or secondary index cluster for read fan-out.
- Counter contention: millions of concurrent increments to a single counter row. Use Redis INCR with periodic DB sync, or a counter sharding approach (multiple partial counter rows summed at read time).
- List pagination: follower lists for celebrities can be billions of rows. Cursor-based pagination using (followee_id, follower_id) as the cursor avoids OFFSET scans. Store the list in a sorted set in Redis for O(log n) range queries.
Sharding Strategy
Two query patterns pull in opposite directions:
- Following list (who does A follow?) — wants rows grouped by follower_id.
- Follower list (who follows B?) — wants rows grouped by followee_id.
Solutions:
- Dual-write: write each follow to two tables, one sharded by follower_id and one by followee_id. Adds write complexity but both reads are local.
- Global secondary index: shard by follower_id; maintain a GSI on followee_id (as in DynamoDB or Cassandra with a separate table).
- Graph database: model users as nodes and follows as directed edges. Neo4j or Amazon Neptune handles both directions efficiently and enables multi-hop traversal for recommendations.
Follow Recommendations
Classic signals for who to recommend:
- Friends of friends: users followed by people you follow, ranked by overlap count. Computable with a graph traversal or a SQL query: SELECT followee_id, COUNT(*) as mutual FROM follows WHERE follower_id IN (SELECT followee_id FROM follows WHERE follower_id=ME) GROUP BY followee_id ORDER BY mutual DESC.
- Contact book matching: hash phone numbers / emails at upload; join against a hashed contact table.
- Interest graph: cluster users by the topics they engage with; recommend across clusters.
- Collaborative filtering: matrix factorization or embedding-based models trained on follow graph edges.
Recommendations are typically computed offline by a batch job or a streaming ML pipeline, stored in a recommendations table, and served read-only by the follow service API.
Private Accounts and Follow Requests
State machine for a follow relationship to a private account:
[none] --follow--> [pending] --approve--> [active]
|
+--reject/withdraw--> [none]
[active] --unfollow--> [none]
[active] --block--> [blocked]
The pending state is stored in the follows table as status='pending'. The followee receives a notification and can approve or reject. Only active rows are counted in follower counts and used for feed generation.
Blocking
- When A blocks B: remove any active follow in either direction, insert a block record, prevent future follow attempts.
- Store blocks in a separate blocks table to keep the follows table clean.
- All API endpoints must check the block table before returning results or processing requests.
Caching
- Is-following: cache in Redis as a hash: HSET user:A:following B 1. Invalidate on unfollow. Use a Bloom filter for accounts with large following lists to quickly answer negative queries.
- Follower/following lists: store as a Redis sorted set keyed by (user_id, direction), scored by follow timestamp. Supports paginated range queries. Cap at ~5000 entries per key; fall back to DB for the long tail.
- Counts: cache in Redis with a write-through policy. Expire after 60 seconds to bound staleness.
API Design
POST /v1/follows body: {follower_id, followee_id}
DELETE /v1/follows body: {follower_id, followee_id}
GET /v1/follows/check?follower_id=A&followee_id=B
GET /v1/users/{id}/followers?cursor=...&limit=50
GET /v1/users/{id}/following?cursor=...&limit=50
GET /v1/users/{id}/counts -- returns follower_count, following_count
GET /v1/users/{id}/recommendations
POST /v1/follows/requests/{id}/approve
POST /v1/follows/requests/{id}/reject
Consistency and Transactions
- The follow insert and counter increment should be atomic. Use a DB transaction or, if counters are in Redis, use a distributed transaction pattern or accept the rare counter inconsistency and reconcile periodically.
- Emit follow events to Kafka after the DB write commits. Use the outbox pattern to guarantee at-least-once event delivery without two-phase commit.
Interview Discussion Points
- Why not use a graph database for everything? Graph DBs excel at traversal but may struggle with write throughput at Twitter scale; most large social networks use sharded relational or key-value stores for the hot path.
- How do you handle a user with 100M followers unfollowing someone? The unfollow itself is a single row delete; the expensive part is propagating the change to the feed service, which uses lazy fan-out for celebrities.
- How do you prevent follow spam bots? Rate limit follow actions per user per hour, detect suspicious patterns (following thousands of accounts in minutes), and integrate with an abuse detection service.
See also: Meta Interview Guide 2026: Facebook, Instagram, WhatsApp Engineering