Referral System Low-Level Design

Requirements

  • Users share a unique referral code or link to invite new users
  • Referrer earns a reward when a referred user completes a qualifying action (signup, first purchase)
  • Support one-sided and two-sided rewards (both referrer and referee get rewards)
  • Prevent fraud: referral fraud, self-referrals, fake accounts
  • 100M users, 1M referrals/month

Data Model

ReferralCode(code VARCHAR UNIQUE, referrer_id UUID, created_at,
             uses_count INT, max_uses INT NULL)

Referral(referral_id UUID, referral_code VARCHAR, referrer_id UUID,
         referee_id UUID NULL,  -- NULL until referee signs up
         status ENUM(PENDING,QUALIFIED,REWARDED,FRAUD,EXPIRED),
         click_at TIMESTAMP, signup_at TIMESTAMP, qualified_at TIMESTAMP,
         ip_address, device_fingerprint, utm_source)

ReferralReward(reward_id UUID, referral_id UUID, user_id UUID,
               reward_type ENUM(CREDIT,DISCOUNT,CASH,POINTS),
               amount DECIMAL, status ENUM(PENDING,ISSUED,EXPIRED),
               issued_at, expires_at)
  1. Referrer requests their referral code: GET /referral/code → generate or return existing code
  2. Referrer shares URL: https://app.example.com/signup?ref=ABC123
  3. Prospect clicks link → server stores referral_code in cookie/session (TTL=30 days)
  4. Prospect signs up → read referral_code from cookie → create Referral record (status=PENDING)
  5. Qualifying action completed (first purchase) → update status=QUALIFIED → issue rewards

Reward Issuance

Rewards should be issued asynchronously after the qualifying event:

def on_qualifying_action(user_id, order_id):
    referral = db.query("SELECT * FROM Referral WHERE referee_id = :uid
                         AND status = 'PENDING'", uid=user_id).first()
    if not referral: return

    # Issue rewards in a transaction
    BEGIN TRANSACTION
    UPDATE Referral SET status='QUALIFIED', qualified_at=NOW() WHERE referral_id=:id
    INSERT INTO ReferralReward (referral_id, user_id=referral.referrer_id, amount=10, status='PENDING')
    INSERT INTO ReferralReward (referral_id, user_id=referral.referee_id, amount=5, status='PENDING')
    COMMIT

    # Apply rewards to accounts (async via job queue)
    enqueue_reward_job(referral_id)

Fraud Prevention

Referral fraud is common and costly. Key fraud signals:

  • Self-referral: same user creates a new account and refers themselves. Detection: same email domain, same IP address, same device fingerprint, same payment method as the referrer. Block if any match.
  • Fake accounts: mass creation of fake accounts to farm referral rewards. Detection: velocity limit (max N signups from same IP per day), email domain blacklist (disposable email providers), phone number verification required for reward.
  • Reward farming rings: coordinated groups of real users cross-referring. Detection: graph analysis — a cluster of users all referring each other is suspicious.
  • Referral abuse: referring using shared/public links (posted on deal sites). Detection: velocity limit on a single referral code (max uses/day).

Fraud holds: instead of issuing rewards immediately, hold for 7-30 days (cooling period) to allow fraud detection. If account is flagged as fraud within the holding period, cancel the reward.

Attribution

Attribution determines which referral source gets credit when a user has multiple touch points. Last-touch attribution (default): the referral code from the most recent click gets credit. First-touch attribution: the first referral code the user clicked gets credit. The referral_code stored in the cookie is typically last-touch — overwrite with new code on each click. Store click history for analysis and dispute resolution.

Referral Code Generation

Codes must be short (for sharing), unique, and hard to guess. Options: (1) Random alphanumeric: 8 characters from [A-Z0-9] = 36^8 ≈ 2.8 trillion combinations. Collision probability with 100M users: < 0.001%. (2) User-ID-based encode: base62-encode the user_id. Shorter, deterministic, but exposes user ID count. (3) Vanity codes: let users set custom codes ("JOHN10"). Store in ReferralCode with UNIQUE constraint. Generate code on first request; cache in Redis key=referral_code:{user_id} permanently.

Key Design Decisions

  • Cookie attribution with 30-day TTL — captures organic referrals without requiring immediate signup
  • Async reward issuance with fraud hold — prevents paying out before fraud is detected
  • Device fingerprint + IP check for self-referral detection — primary fraud signal
  • Velocity limits on code use and signups per IP — blocks mass fake account creation
  • Two-sided rewards — incentivizes both sharing and completing the qualifying action

Shopify system design covers referral and growth programs. See common questions for Shopify interview: referral and growth system design.

Uber system design covers referral programs and driver/rider incentives. Review patterns for Uber interview: referral and incentive system design.

Airbnb system design covers referral growth systems. See design patterns for Airbnb interview: referral and growth system design.

Scroll to Top