Design referral tracking from invite to conversion

Tests data modeling for multi-stage conversion tracking. Strong answers separate codes from conversion events, model status transitions, and enforce unique constraints. Weak answers merge invite and reward into one table or store denormalized counts on users.
What's really being asked
The interviewer wants to see if you can model a stateful business process across multiple tables rather than treating a referral as a single boolean flag. The core challenge is separating the referral code, the referral relationship, and the reward payout into distinct entities with proper constraints, while designing APIs that handle the full lifecycle from link generation to conversion triggering.
The full answer
First, data models: a users table; a referral_codes table with columns code, user_id as referrer, created_at, and a unique constraint on code to prevent collisions; a referrals table with referrer_id, invitee_id, code_used, status as enum like pending or converted, converted_at, and created_at; and a rewards table with referral_id, amount, recipient_id, and an idempotency_key to ensure exactly-once payout. Second, API endpoints: POST generateCode which creates a referral_codes row or returns an existing one for the authenticated user; POST applyCode called during signup with body code and new_user_id to write a pending row into referrals; POST markConverted triggered by the first transaction event to flip status to converted and enqueue a reward job; and GET referrals to list conversions with filters. Third, concurrency controls: use database unique constraints on referral_codes.code to handle race conditions during generation, and use the rewards idempotency_key or a composite unique constraint on referral_id plus recipient_id to prevent double payout. Fourth, status machine clarity: distinguish between signed up and converted because the business rule says rewards only flow after the first transaction, not at registration.
The mistakes people make
Storing a referral_count integer on the users table instead of normalizing referrals; this loses audit history and creates update race conditions. Collapsing the referral event and reward into one table, which makes it impossible to retry payouts independently if a downstream payment provider fails. Failing to enforce unique constraints on generated codes, leading to silent overwrites or collisions under load. Designing a single markReferral endpoint that both creates the user and pays out immediately, which breaks when the first transaction requirement is introduced later.
What usually comes next
How would you prevent self-referral or fraud rings? How would you scale code generation if you need millions of codes per hour? How do you handle reward payout failures or reconcile partial transactions? Would you use an async queue for reward distribution, and how do you guarantee at-least-once delivery without duplicate payouts?
A concrete example
A user with email alice at example dot com calls POST generateCode and receives code A1B2C. The system inserts into referral_codes with user_id pointing to Alice. Bob signs up using code A1B2C; the signup service calls POST applyCode which writes a referrals row with status pending. When Bob completes his first transaction, the transaction service emits an event that triggers POST markConverted for that referral row, flipping status to converted and inserting a rewards row with amount 201 for Alice and amount 51 for Bob, each with a unique idempotency key derived from the transaction ID.
Interview question
Which design best handles the requirement that reward payouts must be retried independently when downstream payment providers fail?
- a.Use a separate rewards table with an idempotency_key and enqueue payout jobs asynchronouslyCorrect
- b.Enforce a unique constraint on the referral code in the referrals table rather than in a separate codes table
- c.Store payout status as a column in the referrals table and update it after each attempt
- d.Add a referral_count column to users and decrement it on payout failure to trigger a retry
Why? this is the answer
The card recommends a separate rewards table with an idempotency_key so failed payouts can be retried independently without duplicates. Option C represents the anti-pattern of collapsing reward state into the referrals table, which prevents isolated retries.
Just read this? Test yourself on what you have been reading.
Read the original → dev.to
- #referral system
- #api design
- #data modeling
- #system design
- #growth
You just looked this up. Could you explain it out loud?
That is the part interviews actually test. Tezvyn takes questions like this one and gives you what the interviewer is really checking, the answer that lands, and the mistake that ends the conversation, in the four minutes before your next meeting.
The iPhone app is on the way
We are building it. Until it lands, nothing here is held back from you: every interview card, your saved cards, streaks and the job board all work in Safari, plus hundreds of free practice quizzes of thirty questions each. Sign in and it all carries over to the app the day it arrives.
Want it as an icon? Tap Share at the bottom of Safari, then Add to Home Screen. It opens full screen and the cards you have read stay available offline.
We are hiring for this. Every open role lists the topics its interview covers, so you can prepare for the real thing rather than guessing.
See open roles