# Invitation System Low-Level Design: Referral Codes, Attribution, and Rewards

Source: https://www.techinterview.org/post/3233468238/lld-invitation-system/
Updated: 2026-04-17 · techinterview.org

An invitation system lets existing users invite new users via unique links or codes, tracking who invited whom and rewarding successful conversions. The engineering challenges are: generating unguessable unique codes, preventing abuse (one user spamming thousands of invites), enforcing single-use semantics, and attributing conversions accurately even when users sign up days after clicking the invite link. These problems appear in referral programs, team workspace onboarding, and closed-beta access flows.

## Core Data Model


```
CREATE TABLE Invitation (
    invite_id       UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    code            VARCHAR(16) NOT NULL UNIQUE,  -- short, URL-safe, unguessable
    inviter_id      BIGINT NOT NULL REFERENCES User(id),
    invitee_email   VARCHAR(255),                 -- NULL for generic link invites
    invite_type     VARCHAR(20) NOT NULL DEFAULT 'email',  -- 'email', 'link', 'code'
    status          VARCHAR(20) NOT NULL DEFAULT 'pending',
    -- pending, accepted, expired, revoked
    max_uses        INT NOT NULL DEFAULT 1,       -- 1 for personal, N for team links
    use_count       INT NOT NULL DEFAULT 0,
    expires_at      TIMESTAMPTZ,
    accepted_at     TIMESTAMPTZ,
    invitee_id      BIGINT REFERENCES User(id),   -- set when accepted
    reward_issued   BOOLEAN NOT NULL DEFAULT FALSE,
    created_at      TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_invite_code ON Invitation(code) WHERE status = 'pending';
CREATE INDEX idx_invite_inviter ON Invitation(inviter_id, created_at DESC);
CREATE INDEX idx_invite_email ON Invitation(invitee_email) WHERE status = 'pending';

-- Rate limiting: track invites sent per user per day
CREATE TABLE InviteRateLimit (
    user_id     BIGINT NOT NULL,
    date        DATE NOT NULL,
    count       INT NOT NULL DEFAULT 0,
    PRIMARY KEY (user_id, date)
);
```


## Code Generation


```
import secrets
import base64

def generate_invite_code(length: int = 10) -> str:
    """Generate a URL-safe, unguessable invite code."""
    # secrets.token_bytes gives cryptographically secure random bytes
    # base64 url-safe encoding, strip padding
    raw = secrets.token_bytes(8)  # 64 bits of entropy
    code = base64.urlsafe_b64encode(raw).decode().rstrip('=')
    return code[:length]  # e.g., "aB3kR9xZmQ"

def create_invitation(inviter_id: int, invitee_email: str = None,
                      max_uses: int = 1, expires_hours: int = 72) -> dict:
    # Rate limit: max 50 invites per user per day
    today = date.today()
    result = db.execute("""
        INSERT INTO InviteRateLimit (user_id, date, count)
        VALUES (%s, %s, 1)
        ON CONFLICT (user_id, date) DO UPDATE
        SET count = InviteRateLimit.count + 1
        RETURNING count
    """, [inviter_id, today])
    if result['count'] > 50:
        raise RateLimitError("Invite limit reached for today")

    # Generate unique code (retry on collision, extremely rare)
    for _ in range(5):
        code = generate_invite_code()
        try:
            invite = db.fetchone("""
                INSERT INTO Invitation (code, inviter_id, invitee_email, max_uses, expires_at)
                VALUES (%s, %s, %s, %s, NOW() + INTERVAL '%s hours')
                RETURNING *
            """, [code, inviter_id, invitee_email, max_uses, expires_hours])
            return invite
        except UniqueViolationError:
            continue  # code collision — retry

    raise RuntimeError("Failed to generate unique invite code")
```


## Invite Acceptance and Atomic Use Counting


```
def accept_invitation(code: str, new_user_id: int) -> dict:
    with db.transaction():
        # Lock the row to prevent concurrent acceptance
        invite = db.fetchone("""
            SELECT * FROM Invitation
            WHERE code = %s AND status = 'pending'
              AND (expires_at IS NULL OR expires_at > NOW())
            FOR UPDATE
        """, [code])

        if not invite:
            raise InvalidInviteError("Invite not found, expired, or already used")

        if invite['use_count'] >= invite['max_uses']:
            raise InviteExhaustedError("This invite link has reached its use limit")

        # Check invitee hasn't already accepted an invite
        existing = db.fetchone(
            "SELECT 1 FROM Invitation WHERE invitee_id = %s", [new_user_id]
        )
        if existing:
            raise DuplicateAcceptError("User already accepted an invitation")

        # Atomically increment use_count
        db.execute("""
            UPDATE Invitation
            SET use_count = use_count + 1,
                invitee_id = CASE WHEN max_uses = 1 THEN %s ELSE invitee_id END,
                status = CASE WHEN use_count + 1 >= max_uses THEN 'accepted' ELSE status END,
                accepted_at = CASE WHEN use_count + 1 >= max_uses THEN NOW() ELSE accepted_at END
            WHERE invite_id = %s
        """, [new_user_id, invite['invite_id']])

    # Issue reward to inviter (async, non-blocking)
    if not invite['reward_issued']:
        reward_queue.enqueue('issue_invite_reward', invite_id=str(invite['invite_id']))
```


## Attribution and Reward Flow


```
def issue_invite_reward(invite_id: str):
    """Called by background worker. Idempotent."""
    invite = db.fetchone("SELECT * FROM Invitation WHERE invite_id = %s", [invite_id])

    if not invite or invite['reward_issued']:
        return  # already issued or invalid

    # Verify the invitee completed the required action (e.g., first purchase)
    invitee_qualified = check_invitee_qualification(invite['invitee_id'])
    if not invitee_qualified:
        return  # invitee hasn't earned the reward yet -- retry later

    with db.transaction():
        # Idempotency guard
        affected = db.execute("""
            UPDATE Invitation SET reward_issued = TRUE
            WHERE invite_id = %s AND reward_issued = FALSE
        """, [invite_id])
        if affected == 0:
            return  # already issued in a concurrent call

        # Credit the inviter
        db.execute("""
            INSERT INTO Reward (user_id, amount, reason, reference_id)
            VALUES (%s, 10.00, 'invite_reward', %s)
        """, [invite['inviter_id'], invite_id])

        db.execute("""
            UPDATE UserAccount SET credit_balance = credit_balance + 10.00
            WHERE user_id = %s
        """, [invite['inviter_id']])
```


## Key Interview Points

- Use `secrets.token_bytes` (cryptographically secure), not `random`, for invite code generation. Predictable codes allow attackers to enumerate valid invites.

- FOR UPDATE on acceptance prevents two concurrent requests from both accepting a single-use invite — the second request sees the already-incremented use_count and fails.

- Reward issuance must be idempotent — the UPDATE WHERE reward_issued=FALSE guard prevents double-credit even if the reward job runs twice.

- Rate limit invites per user: without limits, a single account can generate millions of codes for spam or SEO manipulation. Use the InviteRateLimit table with daily counts.

- Link invites (max_uses > 1) need a different model than personal invites (max_uses=1): the invitee_id column should be NULL for link invites until use_count reaches max_uses.

- Track invite attribution even for delayed signups: store the invite code in a cookie when the link is clicked; retrieve it during registration even if the user signs up days later.

Invitation system and referral program design is discussed in [Airbnb system design interview questions](https://www.techinterview.org/post/3233460279/airbnb-interview-guide-2026-search-systems-trust-and-safety-and-full-stack-engineering/).

Invitation system and referral attribution design is covered in [Uber system design interview preparation](https://www.techinterview.org/post/3233460273/uber-interview-guide-2026-dispatch-systems-geospatial-algorithms-and-marketplace-engineering/).

Invitation system and referral reward design is discussed in [Coinbase system design interview guide](https://www.techinterview.org/post/3233459920/coinbase-interview/).
