Waitlist System Low-Level Design

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.

Scroll to Top