Follow System Low-Level Design

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.

Scroll to Top