# Low Level Design: Waitlist Service

Source: https://www.techinterview.org/post/3233471959/lld-waitlist-service/
Updated: 2026-04-18 · techinterview.org

## What Is a Waitlist Service?

A waitlist service manages an ordered queue of users waiting for access to a product, feature, or limited resource. It tracks position, supports priority manipulation (referrals, tiers), admits users in batches, sends notifications on admission, and defends against gaming.

## Functional Requirements

- Users can join a waitlist and receive a confirmation with their position

- Admins can admit N users at once (batch admission)

- Referrals move a user up the list (priority boost)

- Users receive a notification (email / push) when admitted

- Users can check their current position at any time

- Admins can view list stats and manually admit or remove users

## Non-Functional Requirements

- Position reads should be fast (< 100 ms)

- Joins and priority updates must be consistent — no two users at the same effective position

- Admission is idempotent — re-running admission does not re-admit already-admitted users

- Anti-gaming: referral loops and fake accounts should not yield unbounded priority gains

- Scale to millions of waitlist entries per waitlist

## Core Entities

| Entity | Key Fields |
| --- | --- |
| Waitlist | id, name, description, status ('open'|'closed'|'paused') |
| Entry | id, waitlist_id, user_id, status ('waiting'|'admitted'|'removed'), priority_score, joined_at, admitted_at, referral_code, referred_by |
| Referral | id, referrer_entry_id, referee_entry_id, created_at, boost_applied |
| Notification | id, entry_id, type ('join_confirm'|'position_update'|'admitted'), sent_at, channel |

## Priority Scoring and Ordering

Ordering is determined by a **priority score** (higher = closer to the front) with `joined_at` as a tiebreaker. Base score is 0 at join time. Each confirmed referral adds a fixed boost (e.g., +100 points, capped per user). Tier or invite-only users can receive an initial score offset. The effective sort key is `(priority_score DESC, joined_at ASC)`.

Position is computed as:


```
SELECT COUNT(*) + 1
FROM entries
WHERE waitlist_id = :wid
  AND status = 'waiting'
  AND (priority_score > :my_score
       OR (priority_score = :my_score AND joined_at < :my_joined_at))
```


For large lists this count query is expensive. Cache position in Redis (sorted set) with `ZRANK`: score = `priority_score * 1e12 - epoch_ms` so lexicographic rank matches logical position.

## Batch Admission Flow


```
Admin triggers admit(waitlist_id, n)
  |
  v
SELECT top-N entries WHERE status = 'waiting'
ORDER BY priority_score DESC, joined_at ASC
FOR UPDATE SKIP LOCKED          -- prevents concurrent double-admit
  |
  v
UPDATE entries SET status = 'admitted', admitted_at = NOW()
  |
  v
Publish admission events to queue (Kafka / SQS)
  |
  v
Notification worker: send email/push per user
  |
  v
Remove admitted entries from Redis sorted set
  |
  v
Recompute / invalidate cached positions for top-K remaining users
```


## Referral-Based Priority Boosts

Each entry gets a unique referral code on join. When a new user joins with `ref=CODE`, a Referral row is created linking referee to referrer. On join confirmation (or email verification to reduce fraud), the referrer's `priority_score` is incremented and the Redis sorted set is updated atomically. Boost is capped (e.g., max +500 total from referrals) to prevent runaway gaming.

## Anti-Gaming Measures

- **Email verification required** before a referral boost is credited

- **Per-account referral cap:** max N boosts regardless of referrals count

- **Referral loop detection:** A refers B refers A — detect cycles in the Referral graph and reject or nullify the loop leg

- **Device / IP deduplication:** flag entries from the same device fingerprint or IP subnet as suspicious; hold boosts for manual review

- **Velocity limits:** cap how many referrals can be credited per hour per referrer

## Notification on Admission

Admission events are published to a queue. A notification worker consumes them, generates a personalized access link (time-limited signed token), and sends via email and/or push. Idempotency key = `(entry_id, 'admitted')` prevents duplicate sends if the worker retries. Notification status is persisted so the admin dashboard can show delivery confirmation.

## Scaling Considerations

- **Position reads at scale:** Redis sorted set with O(log N) ZRANK; no DB query for position checks

- **Write contention during admission:** `SKIP LOCKED` in Postgres allows parallel admission batches without deadlocks

- **Priority score updates:** atomic ZINCRBY in Redis + DB update in a transaction; Redis is source of truth for ordering, DB is source of truth for persistence

- **Millions of entries:** shard sorted set by waitlist_id; archive admitted/removed entries to cold storage

## Interview Talking Points

- Why use a sorted set in Redis instead of computing position from DB every time?

- How do you keep Redis and Postgres consistent if the process crashes mid-update?

- How do you handle the referral cap and loop detection without a full graph traversal on every join?

- What happens if two admin threads try to admit the same batch simultaneously? (SKIP LOCKED)

- How would you support multiple waitlists with different admission rules (FIFO vs. priority vs. lottery)?

See also: [Airbnb Interview Guide 2026: Search Systems, Trust and Safety, and Full-Stack Engineering](https://www.techinterview.org/post/3233460279/airbnb-interview-guide-2026-search-systems-trust-and-safety-and-full-stack-engineering/)

See also: [Stripe Interview Guide 2026: Process, Bug Bash Round, and Payment Systems](https://www.techinterview.org/post/3233460268/stripe-interview-guide-2026-process-bug-bash-round-and-payment-systems/)

See also: [Shopify Interview Guide](https://www.techinterview.org/post/3233459924/shopify-interview/)
