Waitlist System — Low-Level Design
A waitlist system queues users for access to a limited resource: early access to a product, sold-out event tickets, or oversubscribed appointment slots. It manages queue position, invite generation, and expiry of unused spots. This design is asked at Airbnb, Coinbase, and early-stage product companies where controlled rollouts are common.
Core Data Model
WaitlistEntry
id BIGSERIAL PK
waitlist_id BIGINT NOT NULL -- which waitlist
user_id BIGINT -- null if anonymous
email TEXT NOT NULL
position INT -- queue position, assigned on join
status TEXT DEFAULT 'waiting' -- waiting, invited, accepted, expired, removed
referral_code TEXT UNIQUE -- user's shareable code
referred_by TEXT -- referral code used on signup
priority_score INT DEFAULT 0 -- higher = moves up the queue
joined_at TIMESTAMPTZ NOT NULL
invited_at TIMESTAMPTZ
accepted_at TIMESTAMPTZ
invite_expires_at TIMESTAMPTZ
Waitlist
id BIGSERIAL PK
name TEXT NOT NULL -- 'Beta Access', 'Event #42'
capacity INT NOT NULL -- total spots available
spots_released INT DEFAULT 0 -- how many invites sent so far
invite_window_hours INT DEFAULT 48 -- hours to accept before invite expires
created_at TIMESTAMPTZ
Joining the Waitlist
def join_waitlist(waitlist_id, email, referral_code=None):
# Idempotency: same email joins only once
existing = db.get_by(WaitlistEntry, waitlist_id=waitlist_id, email=email)
if existing:
return {'position': existing.position, 'status': existing.status}
referred_by = referral_code # store who referred them
# Assign position atomically
with db.transaction():
position = db.execute("""
SELECT COALESCE(MAX(position), 0) + 1
FROM WaitlistEntry WHERE waitlist_id=%(wid)s
""", {'wid': waitlist_id}).scalar()
entry = WaitlistEntry.create(
waitlist_id=waitlist_id,
email=email,
position=position,
referral_code=generate_referral_code(),
referred_by=referred_by,
joined_at=now(),
)
# Boost referrer's priority
if referred_by:
db.execute("""
UPDATE WaitlistEntry
SET priority_score = priority_score + 5
WHERE referral_code=%(code)s AND waitlist_id=%(wid)s
""", {'code': referred_by, 'wid': waitlist_id})
return {'position': position, 'referral_code': entry.referral_code}
Sending Invites
def release_invites(waitlist_id, count):
"""Invite the next `count` eligible users from the waitlist."""
with db.transaction():
# Lock the waitlist to prevent concurrent releases
waitlist = db.execute("""
SELECT * FROM Waitlist WHERE id=%(id)s FOR UPDATE
""", {'id': waitlist_id}).first()
if waitlist.spots_released + count > waitlist.capacity:
count = waitlist.capacity - waitlist.spots_released
if count <= 0:
return 0
# Select next batch: sort by priority_score DESC, then position ASC
entries = db.execute("""
UPDATE WaitlistEntry
SET status='invited',
invited_at=NOW(),
invite_expires_at=NOW() + INTERVAL '%(hours)s hours'
WHERE id IN (
SELECT id FROM WaitlistEntry
WHERE waitlist_id=%(wid)s AND status='waiting'
ORDER BY priority_score DESC, position ASC
LIMIT %(count)s
)
RETURNING email, id
""", {'wid': waitlist_id, 'count': count,
'hours': waitlist.invite_window_hours})
db.execute("""
UPDATE Waitlist SET spots_released=spots_released+%(count)s
WHERE id=%(id)s
""", {'count': len(entries), 'id': waitlist_id})
for entry in entries:
send_invite_email(entry.email, entry.id)
return len(entries)
Accepting an Invite
def accept_invite(invite_token):
entry = db.get_by(WaitlistEntry, invite_token=invite_token)
if not entry or entry.status != 'invited':
raise InvalidInvite()
if entry.invite_expires_at < now():
# Expire this invite and re-release the spot
db.execute("UPDATE WaitlistEntry SET status='expired' WHERE id=%(id)s",
{'id': entry.id})
release_invites(entry.waitlist_id, 1) # give spot to next person
raise InviteExpired()
db.execute("""
UPDATE WaitlistEntry SET status='accepted', accepted_at=NOW()
WHERE id=%(id)s AND status='invited'
""", {'id': entry.id})
# Provision access
grant_access(entry.email, entry.waitlist_id)
return {'access_granted': True}
Queue Position Display
def get_waitlist_position(waitlist_id, email):
entry = db.get_by(WaitlistEntry, waitlist_id=waitlist_id, email=email)
if not entry or entry.status != 'waiting':
return None
# Count people ahead in the queue (accounting for priority)
ahead = db.execute("""
SELECT COUNT(*) FROM WaitlistEntry
WHERE waitlist_id=%(wid)s
AND status='waiting'
AND (priority_score > %(prio)s
OR (priority_score = %(prio)s AND position < %(pos)s))
""", {'wid': waitlist_id, 'prio': entry.priority_score,
'pos': entry.position}).scalar()
return {
'position': ahead + 1,
'referral_code': entry.referral_code,
'referrals_made': count_referrals(entry.referral_code),
}
Key Interview Points
- Position vs. rank: Store the immutable join position but display the effective rank (accounting for referral boosts). These are two different numbers — confusing them causes user-facing display bugs.
- Lock at invite release time: SELECT … FOR UPDATE on the Waitlist row prevents two concurrent release jobs from sending more invites than the capacity allows.
- Expired invite recirculation: When an invite expires, immediately release a new invite to maintain throughput. Don’t wait for the next scheduled release job.
- Referral fraud: Cap the priority boost per referrer (e.g., max +50 points) and require referees to complete signup before the boost counts, to prevent abuse via fake referrals.
Waitlist and controlled rollout system design is discussed in Airbnb system design interview questions.
Waitlist and early access system design is covered in Coinbase system design interview preparation.
Waitlist and invite-based signup system design is discussed in LinkedIn system design interview guide.