Tinder
Design a location-aware discovery and matching service with profile cards, likes, mutual matches, and messaging handoff.
Interview scope and guarantees
Show a bounded set of nearby eligible profiles, record likes or passes, create a match when likes are mutual, and let matched users start a conversation. Location and recommendations may be stale; match identity and blocks require enforced rules.
Capacity worksheet
Assume 5 million daily users perform 100 swipes: 500 million/day, about 5,787/s average and 28,935/s at 5× peak. A 100-byte swipe record adds 50 GB/day raw. Do not compute pairwise scores for all users; retrieve a small eligible candidate set first.
Concrete API contract
GET /recommendations?cursor=...
POST /swipes {targetId,decision,requestId}
GET /matches?cursor=...
POST /blocks {targetId}Data model and access paths
swipes(actor_id,target_id,decision,version); UNIQUE(actor_id,target_id)
matches(match_id PK,low_user_id,high_user_id,generation,state,created_at); UNIQUE(low_user_id,high_user_id,generation)
UNIQUE(low_user_id,high_user_id) WHERE state=active
profiles(user_id PK, coarse_cell, preferences, updated_at)
blocks(actor_id,target_id)
pair_state(low_id,high_id,policy_version,match_generation); PK(low_id,high_id)
conversations(id PK,match_id UNIQUE)
exposures(viewer_id,candidate_id,last_seen_at); PK(viewer_id,candidate_id)Evolve a solution and explain each change
Figure — Three architecture decisions for Tinder, including the pressure each introduces.
Step 1: Generate candidates
Query nearby eligible profiles and apply user preferences. A spatial match does not establish mutual interest or consent.
Step 2: Persist directed swipes
Store one decision per actor and target; detect the reciprocal like. Two simultaneous likes can each miss the other without coordination.
Step 3: Commit one match
Serialize a canonical pair or use a transactionally checked pair record; notify asynchronously. Retries and stale recommendations still need privacy filtering.
Responsibility overview
Figure — Connected responsibilities for Tinder. Trace the authoritative and derived paths separately.
Worked end-to-end scenario
User A likes B while B likes A from another region. Canonicalize the unordered pair as (min ID, max ID), route pair decisions to one owner, and record both directed likes. When both likes exist, a unique match row is created once and an outbox schedules notifications. A repeated swipe returns the stored decision. If a block arrives before the notification worker runs, the worker and chat admission must apply the block policy. Discoverability and matching are different paths: a cached profile recommendation is not permission to message someone.
Why these access paths matter
Index candidate profiles by coarse location cells and eligibility filters, then refine by actual distance. Swipe lookup uses actor and target; match uniqueness uses the canonical pair. Hide blocked users during candidate generation and result serving. Exact location exposure is unnecessary for distance filtering; return a product-appropriate approximate distance.
Build the baseline first
- Profile request → eligibility filter → nearby candidates.
- Swipe → durable pair state → mutual-like check.
- Unique match insert → outbox → notifications.
Evolve the design under load
- Location updates → cell index → candidate cache.
- Pair-key routing → serialized mutual-like evaluation.
- Recommendation workers → precomputed bounded batches.
Defend the hardest decision
A naive implementation writes A likes B, checks B likes A, and can miss a match if both transactions read before the other commits. Route both directions to a canonical pair owner, lock the pair row, or use an idempotent reconciler over durable likes. A unique canonical pair key prevents duplicate matches but does not by itself guarantee that a match is eventually discovered.
Failure and recovery analysis
A profile is recommended just before that user blocks the viewer. Revalidate block and eligibility at interaction time; removing a cached recommendation later is insufficient. If match notification publication fails, the match remains visible from durable state and the outbox retries. Notification failure must not delete the match.
Security and privacy boundary
Protect precise location, enforce age and eligibility policies, and rate-limit automated scraping and swiping.
Interview follow-ups with reasoning
Question: How do you avoid repeatedly showing passed profiles?
Show answer and explanation
Answer: Store exclusions and define whether they expire.
Question: How does a popular profile affect partitioning?
Show answer and explanation
Answer: Separate profile read caching from writes keyed by actor or canonical pair.
Question: What is the location privacy boundary?
Show answer and explanation
Answer: Return coarse distance bands rather than exact coordinates.
Operate and verify the design
Recommendation latency, mutual-like reconciliation lag and duplicate-match attempts.
Commit reciprocal likes concurrently and verify eventual creation of exactly one canonical match.
A second scenario to test transfer
A likes B at 10:00, B likes A at 10:01, and the two requests hit different application workers. Each durable Like becomes visible; exactly one transaction wins creation of a canonical pair key. Both clients receive the same match ID. A retried request returns that identity rather than making a second match or notification.
Two opposite-like requests race. How do you create one match and prevent repeated notifications?
Show answer and explanation
Answer: Lock or route both directed actions through the canonical pair authority. Under that authority, persist the unique actor-target Like, read both directions, and insert the unique match and outbox event when mutual. Independent read-after-write checks can miss both concurrent Likes; a unique match constraint alone only prevents duplicates. Recheck blocks before notification and chat admission.
Compare alternatives
| Decision | Benefit | Trade-off |
|---|---|---|
| Coarse candidate pool | Fast discovery | Stale or duplicate candidates |
| Exact final eligibility | Privacy and filters | Read-path checks |
| Canonical pair key | Single mutual match | Pair ordering and retries |
| Exposure cursor | Less repetition | Per-viewer state volume |
A design-changing exercise
Both reciprocal checks return absent before either swipe commits. What prevents a missed match?
Show answer and explanation
Answer: A serialized pair decision or transactional retry/reconciliation on the pair is required. Independent read-then-write checks do not guarantee detection.
Design workshop: serialize the pair, not the whole population
Discovery, swipe, match and authorized chat are core; sophisticated recommendation-model training is an extension. Assume discovery p95 under 300 ms, pair decisions under 200 ms within their home region and no chat admitted after a block is authoritative. Exact locations are not returned to users.
The pair key is ordered(low_user_id,high_user_id). Create pair_state with this primary key using an insert-on-conflict, then lock it before changing directed likes. This is important: locking a row that does not yet exist is not a mutual-exclusion mechanism. Both directions use the same authority and transaction.
BEGIN;
INSERT INTO pair_state(low_id,high_id) VALUES (:low,:high)
ON CONFLICT DO NOTHING;
SELECT * FROM pair_state WHERE low_id=:low AND high_id=:high FOR UPDATE;
-- Verify block/account state under the chosen pair policy.
-- Persist the direction's Like or Pass and stable request result.
-- If both directions now Like, insert one unique match and its outbox.
COMMIT;If A and B like concurrently, the first holder persists one Like; the second sees it after acquiring the pair lock and creates the match. A unique match row alone prevents duplicate matches but cannot make independent read-before-commit checks discover a missing one. A reconciler scanning durable pair changes is still useful repair, not a substitute for explaining the chosen concurrency rule.
Figure — Concurrent likes under one pair lock.
A block command locks the same pair record and increments its policy version. Candidate caches may still contain the profile, but response filtering and chat admission consult the current block/account policy. Notification after a block suppresses content rather than treating an old match event as permission. An unmatch closes the existing match; whether future reciprocal Likes can create a new match is a chosen generation policy. For this scenario require explicit new Likes after unmatch; do not replay old Likes into a new chat.
Discovery uses coarse cells and eligibility filters, fetches a bounded candidate pool, computes exact distance internally and ranks by declared preferences. Store exposure(viewer_id,candidate_id,last_seen_at) or a bounded recent-exposure set to reduce repeats. A signed discovery cursor pins a pool/version; an exact location never belongs in that cursor. Pass records expire only under a stated rediscovery policy.
Figure — Match and chat contracts.
Add matches(match_id,pair_key,generation,state) and conversations(match_id UNIQUE,id). Chat storage/streaming can reuse a conversation design, but every send still checks active participant authorization. Do not duplicate all messaging infrastructure in this chapter; clearly state the contract between matching and messaging. Regional writes route to the pair's home shard; failover fences the old writer before another owner accepts commands.
Exercise: Both Likes commit on independent workers without either seeing the other. Is UNIQUE(pair) sufficient?
Show answer and explanation
Answer: No. There may be no attempted match insert. Route or lock the canonical pair as above, or provide explicit eventual reconciliation over both durable Likes. The first solves the primary race; reconciliation repairs missed processing.