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.