Like System Low-Level Design

Like System — Low-Level Design

A like system records user reactions to content and displays counts. At scale, naive approaches (COUNT(*) per item) collapse under traffic. This design is asked at Meta, Twitter, and any social platform — it touches denormalization, cache invalidation, and write amplification at high volume.

Core Data Model

Like
  user_id         BIGINT NOT NULL
  content_id      BIGINT NOT NULL
  content_type    TEXT NOT NULL    -- 'post', 'comment', 'photo'
  created_at      TIMESTAMPTZ
  PRIMARY KEY (user_id, content_id, content_type)

ContentStats
  content_id      BIGINT NOT NULL
  content_type    TEXT NOT NULL
  like_count      BIGINT DEFAULT 0
  PRIMARY KEY (content_id, content_type)

-- Index for "all posts liked by user X" (profile page, activity feed)
CREATE INDEX idx_likes_user ON Like(user_id, created_at DESC);

Toggle Like (Add/Remove)

def toggle_like(user_id, content_id, content_type):
    try:
        db.execute("""
            INSERT INTO Like (user_id, content_id, content_type, created_at)
            VALUES (%(uid)s, %(cid)s, %(ctype)s, NOW())
        """, {'uid': user_id, 'cid': content_id, 'ctype': content_type})

        # Increment count
        db.execute("""
            INSERT INTO ContentStats (content_id, content_type, like_count)
            VALUES (%(cid)s, %(ctype)s, 1)
            ON CONFLICT (content_id, content_type)
            DO UPDATE SET like_count = ContentStats.like_count + 1
        """, {'cid': content_id, 'ctype': content_type})

        # Invalidate cache
        cache.delete(f'like_count:{content_type}:{content_id}')
        cache.delete(f'user_liked:{user_id}:{content_type}:{content_id}')
        return {'liked': True, 'delta': +1}

    except UniqueViolation:
        # Already liked — remove it
        db.execute("""
            DELETE FROM Like
            WHERE user_id=%(uid)s AND content_id=%(cid)s AND content_type=%(ctype)s
        """, {'uid': user_id, 'cid': content_id, 'ctype': content_type})

        db.execute("""
            UPDATE ContentStats SET like_count = GREATEST(0, like_count - 1)
            WHERE content_id=%(cid)s AND content_type=%(ctype)s
        """, {'cid': content_id, 'ctype': content_type})

        cache.delete(f'like_count:{content_type}:{content_id}')
        cache.delete(f'user_liked:{user_id}:{content_type}:{content_id}')
        return {'liked': False, 'delta': -1}

Reading Like Count and User Like Status

def get_content_stats(content_id, content_type, viewer_user_id=None):
    # Like count from cache or DB
    cache_key = f'like_count:{content_type}:{content_id}'
    count = cache.get(cache_key)
    if count is None:
        row = db.execute("""
            SELECT like_count FROM ContentStats
            WHERE content_id=%(cid)s AND content_type=%(ctype)s
        """, {'cid': content_id, 'ctype': content_type}).first()
        count = row.like_count if row else 0
        cache.set(cache_key, count, ttl=300)

    # Has the viewer liked this?
    user_liked = None
    if viewer_user_id:
        user_key = f'user_liked:{viewer_user_id}:{content_type}:{content_id}'
        user_liked = cache.get(user_key)
        if user_liked is None:
            exists = db.execute("""
                SELECT 1 FROM Like
                WHERE user_id=%(uid)s AND content_id=%(cid)s AND content_type=%(ctype)s
            """, {'uid': viewer_user_id, 'cid': content_id, 'ctype': content_type}).first()
            user_liked = bool(exists)
            cache.set(user_key, user_liked, ttl=300)

    return {'like_count': count, 'viewer_liked': user_liked}

Scaling for High-Traffic Content (Viral Posts)

-- Problem: a viral post gets 10,000 likes/second
-- UPDATE ContentStats SET like_count+1 creates a hot row in Postgres

-- Solution 1: Write-behind cache (Redis counter)
def like_with_redis_counter(user_id, content_id, content_type):
    key = f'like_count:{content_type}:{content_id}'
    pipe = redis.pipeline()
    pipe.sadd(f'likers:{content_type}:{content_id}', user_id)  # for dedup
    pipe.incr(key)
    pipe.execute()
    # Flush to DB periodically (every 5 seconds via cron)

def flush_like_counts():
    for key in redis.scan_iter('like_count:*'):
        parts = key.split(':')
        content_type, content_id = parts[1], parts[2]
        count = redis.getset(key, 0)  # reset to 0, get old value
        db.execute("""
            UPDATE ContentStats SET like_count=like_count+%(delta)s
            WHERE content_id=%(cid)s AND content_type=%(ctype)s
        """, {'delta': count, 'cid': content_id, 'ctype': content_type})

Fetching “Who Liked This”

-- List of users who liked a post (paginated, newest first)
SELECT l.user_id, u.display_name, u.avatar_url, l.created_at
FROM Like l
JOIN User u ON l.user_id = u.id
WHERE l.content_id=%(cid)s AND l.content_type=%(ctype)s
ORDER BY l.created_at DESC
LIMIT 20;

-- For counts >1M: don't fetch all likers — show count only
-- Display top 3 names + "{N} others liked this"
SELECT l.user_id FROM Like
WHERE content_id=%(cid)s
  AND user_id IN (%(followed_user_ids)s)  -- friends who liked it
ORDER BY created_at DESC
LIMIT 3;

Key Interview Points

  • PRIMARY KEY (user_id, content_id, content_type): Enforces at-most-one like per user per item at the DB level. Use UniqueViolation on INSERT to detect “already liked” without a separate SELECT.
  • Never COUNT(*) at read time: Maintain like_count in ContentStats. COUNT(*) over a billion-row Likes table is a full index scan taking seconds.
  • Redis write-behind for hot rows: At 10K likes/second, even a fast Postgres UPDATE becomes a bottleneck due to row-level lock contention. Buffer in Redis; flush to Postgres in batches.
  • Separate count from existence check: “How many likes?” hits ContentStats (one row). “Did this user like it?” hits Like with a (user_id, content_id, content_type) PK lookup. Both are O(1).

Like system and social reaction design is discussed in Meta system design interview questions.

Like system and engagement tracking design is covered in Twitter system design interview preparation.

Like system and content reaction design is discussed in Snap system design interview guide.

Scroll to Top