Machine Coding Problem

Follower / Following System

macoAllsocialasymmetric-relation-modeling
Commonly Asked By:MetaTwitterLinkedInPinterest

Requirements & System Scope

Functional Scope (In-Scope)

  • Asymmetric Follow Mechanics: Models unidirectional follow actions cleanly (following someone does not force a reciprocal connection).
  • Dual Adjacency Graph Sets: Maintains separate follower and following directories for O(1) read checks.
  • Mutual Follow Intersects: Intersects follower lists to verify mutual status (critical for locking direct messaging permissions).
  • Count Cache Cache updates: Increments and decrements counter blocks atomically during transactions.

Explicit Boundaries (Out-of-Scope)

  • Direct Activity timeline generators: Simplifies actual news feed timeline building, query pipelines, and caches.
  • User Profile Assets / Block lists: Blocks user avatar renders, profile descriptions, and user reporting endpoints.

Class Diagram & Entity Relationships

Adjacency tables and mutual intersections mapped out:

Loading...
  • FollowRelation: Records follower IDs, followee IDs, and timestamps.
  • FollowService: Drives relationships, graph indexing, mutual searches, and count caches.

Design Patterns & SOLID Principles

  • Graph Representation Design: Implements graph adjacency sets to represent asymmetric nodes and social links.
  • Proxy / Cache Pattern: Protects servers from computing Set.size() on high-follower accounts by tracking metrics in an atomic count cache.
  • Single Responsibility Principle (SRP): Isolates follow state updates from timeline layouts and message routing.

Core Execution Workflows

Atomicity & Celebrity Scale Mitigation

  1. Asymmetric Follow Processing:
    1. Verify target User IDs exist and are not identical.
    2. Add the relationship to the follower's followingMap and the followee's followersMap.
    3. Atomically increment both count caches. Roll back if graph updates fail.
  2. Celebrity Scaling & Feed Lookup Strategy (Interview context):
    1. For standard accounts, push model fan-out updates to follower homefeeds on publish.
    2. For celebrities with millions of followers, avoid write fan-out. Followers instead query celebrity stories dynamically on load (pull model fan-out).

Concurrency & Thread Safety Strategy

Protecting graph states during high-volume follow and unfollow requests:

  • Atomic Counts: Follow count caches use atomic updates to prevent lost updates under parallel operations.
  • Concurrent Graph Sets: Graph edges utilize concurrent sets to support high-throughput lookups without blocking reads.

Complete Clean Code Blueprint

Production reference implementations demonstrating asymmetric relationships, mutual follow intersections, and atomic count caching in Java and Python:

// โ”€โ”€โ”€ JAVA BLUEPRINT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantReadWriteLock;

class FollowRelation {
    private final String followerId;
    private final String followeeId;
    private final long createdAtMs;

    public FollowRelation(String followerId, String followeeId) {
        this.followerId = followerId;
        this.followeeId = followeeId;
        this.createdAtMs = System.currentTimeMillis();
    }

    public String getFollowerId() { return followerId; }
    public String getFolloweeId() { return followeeId; }
    public long getCreatedAtMs() { return createdAtMs; }
}

class FollowService {
    // Bidirectional Adjacency Sets
    private final ConcurrentHashMap<String, Set<String>> followersMap = new ConcurrentHashMap<>(); // userId -> Set of Follower IDs
    private final ConcurrentHashMap<String, Set<String>> followingMap = new ConcurrentHashMap<>(); // userId -> Set of Followee IDs
    
    // Count Caches with Atomic Updates
    private final ConcurrentHashMap<String, AtomicInteger> followerCounts = new ConcurrentHashMap<>();
    private final ConcurrentHashMap<String, AtomicInteger> followingCounts = new ConcurrentHashMap<>();

    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

    // O(1) follow relation insertion with full sync lock
    public boolean follow(String followerId, String followeeId) {
        if (followerId.equals(followeeId)) {
            throw new IllegalArgumentException("Users cannot follow themselves.");
        }

        rwLock.writeLock().lock();
        try {
            // Add to following set of the follower
            Set<String> following = followingMap.computeIfAbsent(followerId, k -> ConcurrentHashMap.newKeySet());
            boolean isNew = following.add(followeeId);
            
            if (!isNew) {
                return false; // Relation already exists
            }

            // Add to followers set of the followee
            Set<String> followers = followersMap.computeIfAbsent(followeeId, k -> ConcurrentHashMap.newKeySet());
            followers.add(followerId);

            // Increment Count Caches atomically
            followingCounts.computeIfAbsent(followerId, k -> new AtomicInteger(0)).incrementAndGet();
            followerCounts.computeIfAbsent(followeeId, k -> new AtomicInteger(0)).incrementAndGet();

            return true;
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    // O(1) follow relation removal
    public boolean unfollow(String followerId, String followeeId) {
        rwLock.writeLock().lock();
        try {
            Set<String> following = followingMap.get(followerId);
            if (following == null || !following.contains(followeeId)) {
                return false; // Relation doesn't exist
            }

            following.remove(followeeId);
            
            Set<String> followers = followersMap.get(followeeId);
            if (followers != null) {
                followers.remove(followerId);
            }

            // Decrement Count Caches atomically
            decrementCount(followingCounts, followerId);
            decrementCount(followerCounts, followeeId);

            return true;
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    public boolean isFollowing(String followerId, String followeeId) {
        rwLock.readLock().lock();
        try {
            Set<String> following = followingMap.get(followerId);
            return following != null && following.contains(followeeId);
        } finally {
            rwLock.readLock().unlock();
        }
    }

    // Mutual follow check
    public boolean isMutual(String userA, String userB) {
        rwLock.readLock().lock();
        try {
            return isFollowing(userA, userB) && isFollowing(userB, userA);
        } finally {
            rwLock.readLock().unlock();
        }
    }

    // Intersection of followers and followees to return mutuals
    public Set<String> getMutualFollows(String userId) {
        rwLock.readLock().lock();
        try {
            Set<String> following = followingMap.get(userId);
            Set<String> followers = followersMap.get(userId);

            if (following == null || followers == null || following.isEmpty() || followers.isEmpty()) {
                return Collections.emptySet();
            }

            // Intersect sets
            Set<String> mutuals = new HashSet<>(following);
            mutuals.retainAll(followers);
            return mutuals;
        } finally {
            rwLock.readLock().unlock();
        }
    }

    public int getFollowerCount(String userId) {
        rwLock.readLock().lock();
        try {
            AtomicInteger count = followerCounts.get(userId);
            return count != null ? count.get() : 0;
        } finally {
            rwLock.readLock().unlock();
        }
    }

    public int getFollowingCount(String userId) {
        rwLock.readLock().lock();
        try {
            AtomicInteger count = followingCounts.get(userId);
            return count != null ? count.get() : 0;
        } finally {
            rwLock.readLock().unlock();
        }
    }

    public Set<String> getFollowers(String userId) {
        rwLock.readLock().lock();
        try {
            Set<String> followers = followersMap.get(userId);
            return followers != null ? new HashSet<>(followers) : Collections.emptySet();
        } finally {
            rwLock.readLock().unlock();
        }
    }

    public Set<String> getFollowing(String userId) {
        rwLock.readLock().lock();
        try {
            Set<String> following = followingMap.get(userId);
            return following != null ? new HashSet<>(following) : Collections.emptySet();
        } finally {
            rwLock.readLock().unlock();
        }
    }

    private void decrementCount(ConcurrentHashMap<String, AtomicInteger> map, String key) {
        AtomicInteger count = map.get(key);
        if (count != null) {
            count.updateAndGet(val -> Math.max(0, val - 1));
        }
    }
}

public class Main {
    public static void main(String[] args) throws Exception {
        System.out.println("=== JAVA FOLLOWER SYSTEM DEMO ===");
        FollowService service = new FollowService();

        // Establish connections
        service.follow("Alice", "Bob");
        service.follow("Alice", "Charlie");
        service.follow("Bob", "Alice"); // Bob also follows Alice (Mutual)
        
        System.out.println("Alice follows Bob: " + service.isFollowing("Alice", "Bob"));
        System.out.println("Bob follows Alice: " + service.isFollowing("Bob", "Alice"));
        System.out.println("Alice and Bob mutual? " + service.isMutual("Alice", "Bob"));
        System.out.println("Alice and Charlie mutual? " + service.isMutual("Alice", "Charlie"));

        System.out.println("Alice follower count: " + service.getFollowerCount("Alice"));
        System.out.println("Alice following count: " + service.getFollowingCount("Alice"));

        System.out.println("Alice mutual follows list: " + service.getMutualFollows("Alice"));

        // Test concurrent follows
        System.out.println("Simulating concurrent follower actions...");
        ExecutorService executor = Executors.newFixedThreadPool(4);
        for (int i = 1; i <= 10; i++) {
            final String followerName = "User_" + i;
            executor.submit(() -> {
                service.follow(followerName, "Bob");
            });
        }

        executor.shutdown();
        executor.awaitTermination(2, TimeUnit.SECONDS);

        System.out.println("Bob total followers (should be 11: Alice + 10 concurrent): " + service.getFollowerCount("Bob"));

        // Unfollow test
        service.unfollow("Alice", "Bob");
        System.out.println("Alice unfollowed Bob. Alice following Bob? " + service.isFollowing("Alice", "Bob"));
        System.out.println("Bob total followers now: " + service.getFollowerCount("Bob"));

        System.out.println("=== END OF JAVA DEMO ===");
    }
}

๐Ÿ’ฌReview

Help Us Improve

How helpful was this walkthrough?

Click a star to rate. We actively use this feedback to refine and update our system design content.

Placeholder
Optional but highly appreciated!

Discussion

Share your thoughts, ask questions, or help others.

Loading comments...