User Blocking System Low-Level Design

User Blocking System — Low-Level Design

A user blocking system prevents one user from seeing or interacting with another. It must enforce blocks across all content surfaces, handle bidirectional visibility, and scale to millions of block relationships. This design is asked at Twitter, Instagram, and any social platform.

Core Data Model

Block
  blocker_id      BIGINT NOT NULL    -- user who initiated the block
  blocked_id      BIGINT NOT NULL    -- user who was blocked
  created_at      TIMESTAMPTZ NOT NULL
  PRIMARY KEY (blocker_id, blocked_id)

-- Index for the reverse lookup: "who has blocked me?"
CREATE INDEX idx_block_blocked ON Block(blocked_id, blocker_id);

-- Checking both directions:
-- A blocks B: (blocker=A, blocked=B)
-- Neither A sees B's content NOR B sees A's content
-- "Has a block relationship between A and B?"
-- SELECT 1 FROM Block WHERE (blocker_id=A AND blocked_id=B)
--                        OR (blocker_id=B AND blocked_id=A)

Blocking and Unblocking

def block_user(blocker_id, blocked_id):
    if blocker_id == blocked_id:
        raise ValidationError('Cannot block yourself')

    db.execute("""
        INSERT INTO Block (blocker_id, blocked_id, created_at)
        VALUES (%(blocker)s, %(blocked)s, NOW())
        ON CONFLICT DO NOTHING
    """, {'blocker': blocker_id, 'blocked': blocked_id})

    # Side effects:
    # 1. Remove follow relationships in both directions
    db.execute("""
        DELETE FROM Follow
        WHERE (follower_id=%(a)s AND followee_id=%(b)s)
           OR (follower_id=%(b)s AND followee_id=%(a)s)
    """, {'a': blocker_id, 'b': blocked_id})

    # 2. Remove from each other's follower counts (denormalized)
    # (handled by the DELETE trigger or application code)

    # 3. Invalidate block check cache
    cache_key = f'block:{min(blocker_id, blocked_id)}:{max(blocker_id, blocked_id)}'
    redis.delete(cache_key)

def unblock_user(blocker_id, blocked_id):
    db.execute("""
        DELETE FROM Block WHERE blocker_id=%(blocker)s AND blocked_id=%(blocked)s
    """, {'blocker': blocker_id, 'blocked': blocked_id})
    cache_key = f'block:{min(blocker_id, blocked_id)}:{max(blocker_id, blocked_id)}'
    redis.delete(cache_key)

Block Check at Read Time

def are_blocked(user_a, user_b):
    """Returns True if either user has blocked the other."""
    cache_key = f'block:{min(user_a, user_b)}:{max(user_a, user_b)}'
    cached = redis.get(cache_key)
    if cached is not None:
        return cached == b'1'

    result = db.execute("""
        SELECT 1 FROM Block
        WHERE (blocker_id=%(a)s AND blocked_id=%(b)s)
           OR (blocker_id=%(b)s AND blocked_id=%(a)s)
        LIMIT 1
    """, {'a': user_a, 'b': user_b}).first()

    blocked = result is not None
    redis.setex(cache_key, 300, '1' if blocked else '0')
    return blocked

Filtering Blocked Users from Feed Queries

-- Option 1: JOIN-based filter (good for small block lists)
SELECT p.* FROM Post p
WHERE p.author_id NOT IN (
    SELECT blocked_id FROM Block WHERE blocker_id=%(viewer)s
    UNION
    SELECT blocker_id FROM Block WHERE blocked_id=%(viewer)s
)
AND p.author_id = %(target)s;

-- Option 2: Pre-loaded block list (good for feed generation)
def get_feed(viewer_id):
    # Load viewer's block list once (usually small, < 1000 entries)
    blocked_users = get_block_list(viewer_id)  # cached in Redis

    # Filter at application level
    posts = get_candidate_posts(viewer_id)
    return [p for p in posts if p.author_id not in blocked_users]

def get_block_list(user_id):
    """All user IDs in a block relationship with this user."""
    key = f'block_list:{user_id}'
    cached = redis.get(key)
    if cached:
        return set(json.loads(cached))

    rows = db.execute("""
        SELECT blocked_id AS other_id FROM Block WHERE blocker_id=%(uid)s
        UNION
        SELECT blocker_id AS other_id FROM Block WHERE blocked_id=%(uid)s
    """, {'uid': user_id})
    ids = {r.other_id for r in rows}
    redis.setex(key, 300, json.dumps(list(ids)))
    return ids

Blocking Side Effects Checklist

When A blocks B, handle all of these:
  [x] Remove A→B follow
  [x] Remove B→A follow
  [x] Hide A's content from B's feed
  [x] Hide B's content from A's feed
  [x] B cannot reply to A's posts
  [x] B cannot DM A
  [x] A's profile is not visible to B (404 or "User not found")
  [x] B's @mentions of A are hidden from A's notifications
  [x] A does not appear in B's search results
  [x] Shared group chats: A and B can still see messages (platform-dependent)
       Most platforms: blocking doesn't affect shared channels/groups

Key Interview Points

  • Bidirectional semantics: A block is one-directional in the DB (blocker → blocked) but has bidirectional visibility effects. When checking “can X see Y’s content?” always check both (X→Y) and (Y→X) in the Block table.
  • Cache the block list, not individual pairs: For feed filtering, loading the full block list once (usually <100 entries) and filtering at the application level is faster than per-post SQL subqueries.
  • Invalidate caches on block/unblock: The block relationship cache must be invalidated immediately on change. A stale cache that shows a blocked user’s content violates the user’s expectations.
  • Remove follows on block: A block should silently remove mutual follows so that unblocking later doesn’t automatically restore the follow relationship without user consent.

User blocking and harassment prevention design is discussed in Twitter system design interview questions.

User blocking and content visibility design is covered in Meta system design interview preparation.

User blocking and friend system design is discussed in Snap system design interview guide.

Scroll to Top