What is a Follow System?
A follow system lets users subscribe to updates from other users or entities: follow a person (Twitter, Instagram), watch a repository (GitHub), subscribe to a topic (Reddit). Key operations: follow/unfollow, get followers, get following, check if A follows B, and recommend who to follow. At scale (hundreds of millions of users with celebrity accounts having 50M+ followers), these operations must be fast and the data structure must handle asymmetric relationships efficiently.
Requirements
- Follow and unfollow: <10ms
- Get followers of user X (paginated)
- Get following list of user X (paginated)
- Check if user A follows user B: <5ms
- Follower count and following count displayed on profile
- Mutual follows (friends): users who follow each other
- 100M users, up to 50M followers per celebrity
Data Model
Follow(
follower_id UUID NOT NULL, -- user who is following
followee_id UUID NOT NULL, -- user being followed
created_at TIMESTAMPTZ NOT NULL,
PRIMARY KEY (follower_id, followee_id)
)
-- Index for both query directions:
CREATE UNIQUE INDEX idx_follow_pk ON Follow(follower_id, followee_id);
CREATE INDEX idx_follow_reverse ON Follow(followee_id, follower_id, created_at DESC);
-- Denormalized counts (updated on every follow/unfollow)
UserStats(
user_id UUID PRIMARY KEY,
follower_count INT DEFAULT 0,
following_count INT DEFAULT 0
)
Follow / Unfollow
def follow(follower_id, followee_id):
if follower_id == followee_id:
raise ValidationError('Cannot follow yourself')
try:
db.insert(Follow(follower_id=follower_id, followee_id=followee_id,
created_at=now()))
# Increment counts atomically
db.execute('UPDATE UserStats SET following_count = following_count + 1 WHERE user_id=?', follower_id)
db.execute('UPDATE UserStats SET follower_count = follower_count + 1 WHERE user_id=?', followee_id)
except UniqueViolation:
pass # already following — idempotent
def unfollow(follower_id, followee_id):
deleted = db.execute(
'DELETE FROM Follow WHERE follower_id=? AND followee_id=?',
follower_id, followee_id
).rowcount
if deleted:
db.execute('UPDATE UserStats SET following_count = following_count - 1 WHERE user_id=?', follower_id)
db.execute('UPDATE UserStats SET follower_count = follower_count - 1 WHERE user_id=?', followee_id)
Check Follow Status (Hot Path)
# DB check: O(1) primary key lookup
def is_following(follower_id, followee_id):
return db.exists(
'SELECT 1 FROM Follow WHERE follower_id=? AND followee_id=?',
follower_id, followee_id
)
# Cache in Redis for profile page rendering (check A→B and B→A on every profile view)
def is_following_cached(follower_id, followee_id):
key = f'follow:{follower_id}:{followee_id}'
cached = redis.get(key)
if cached is not None:
return cached == '1'
result = is_following(follower_id, followee_id)
redis.setex(key, 300, '1' if result else '0')
return result
# On follow/unfollow: invalidate cache
redis.delete(f'follow:{follower_id}:{followee_id}')
Paginated Follower / Following Lists
def get_followers(user_id, cursor=None, limit=50):
# cursor encodes (created_at, follower_id) of the last seen row
query = '''
SELECT follower_id, created_at FROM Follow
WHERE followee_id = :uid
'''
if cursor:
decoded = decode_cursor(cursor)
query += ' AND (created_at, follower_id) < (:ts, :fid)'
query += ' ORDER BY created_at DESC, follower_id DESC LIMIT :limit'
rows = db.query(query, uid=user_id, **cursor_params, limit=limit)
return rows, make_cursor(rows[-1]) if len(rows) == limit else None
def get_mutual_follows(user_id, cursor=None, limit=50):
# Users who follow each other
rows = db.query('''
SELECT f1.followee_id AS friend_id FROM Follow f1
JOIN Follow f2 ON f1.followee_id = f2.follower_id
AND f2.followee_id = f1.follower_id
WHERE f1.follower_id = :uid
ORDER BY f1.created_at DESC
LIMIT :limit OFFSET :offset
''', uid=user_id, limit=limit, offset=offset)
Who to Follow Recommendations
Simple graph-based recommendation: “users followed by people you follow” (2nd-degree connections):
def recommend_follows(user_id, limit=10):
return db.query('''
SELECT followee_id, COUNT(*) AS mutual_count
FROM Follow
WHERE follower_id IN (
SELECT followee_id FROM Follow WHERE follower_id=:uid
)
AND followee_id != :uid
AND followee_id NOT IN (
SELECT followee_id FROM Follow WHERE follower_id=:uid
)
GROUP BY followee_id
ORDER BY mutual_count DESC
LIMIT :limit
''', uid=user_id, limit=limit)
Key Design Decisions
- Composite primary key (follower_id, followee_id) — O(1) follow existence check; unique constraint prevents duplicates
- Reverse index (followee_id, follower_id) — enables efficient “get all followers of X” without full scan
- Denormalized counts in UserStats — follower/following counts are shown on every profile; avoid COUNT(*) queries
- Redis cache for is_following — profile pages check mutual follow status; cache reduces DB load dramatically
- Idempotent follow — INSERT with unique constraint; duplicate follow is a no-op not an error
Follow system and social graph design is discussed in Meta system design interview questions.
Follow system and social graph architecture is covered in Twitter system design interview preparation.
Follow and friend system design is discussed in Snap system design interview guide.