Courses › System Design › Question Breakdowns
Design Tinder
Geo candidate search, precomputed stacks, Bloom filters against repeats and atomic mutual-match detection.
Tinder shows each user a stack of nearby profiles; they swipe right (like) or left (pass), and when two people like each other it's a match and they can chat. The interface is simple, but it's a question about geospatial recommendation at scale, write-heavy swipe storage, detecting mutual likes consistently, and never showing the same profile twice.
Requirements
Functional
- Users can set preferences: age range, gender, maximum distance
- Users get a stack of nearby profiles that match their preferences
- Users can swipe right (like) or left (pass) on each profile
- When two users like each other, both get a match notification and can message
Out of scope: chat itself (see Design WhatsApp), photo moderation, paid features like Super Like and Boost.
Non-functional
- Scale: ~20M daily users, ~2B swipes/day
- Low latency: the stack loads in < 300 ms; swiping feels instant
- Consistency for matches: if both users like each other, a match must be created — exactly once, even if they swipe at the same moment
- No repeats: a user should not see a profile they've already swiped on
- Freshness: location updates within minutes as users move
[!TIP] The insight that frames the design Swiping is the write path (huge volume, simple writes); building the stack is the read path (a geospatial search plus filtering). Precompute stacks in the background so the read path is a cheap pop from a cached list, and keep the match check on the write path tiny and atomic.
Capacity estimates
Core Entities
- Profile — user id, name, age, gender, bio, photos (in blob storage), location (geohash), last active
- Preferences — age range, genders, max distance
- Swipe — swiper, swipee, direction (like / pass), timestamp
- Match — user A, user B (stored as an ordered pair), created time
API
GET /v1/feed?lat=&lng= → profiles[] (a batch of ~50)
POST /v1/swipes { targetUserId, direction: "like" | "pass" }
→ 200 { matched: true, matchId: "m_77" } or { matched: false }
GET /v1/matches → matches[]
PUT /v1/me/location { lat, lng }High-Level Design
user = client "Mobile App" @0,1
gw = service "API Gateway" @1,1
feed = service "Feed Service" @2,0
swipe = service "Swipe Service" @2,1
geo = store "Profile Search\n(geo index)" @3,0
stacks = cache "Precomputed Stacks\n(Redis)" @3,1
swipedb = store "Swipes DB\n(Cassandra)" @3,2
match = store "Matches" @2,2
notif = service "Notifications" @1,2
user -> gw
gw -> feed, swipe
feed -> stacks
feed --> geo : refill
swipe -> swipedb
swipe -> match
match --> notif- Load the stack: the feed service pops the next batch from the user's precomputed stack in Redis, refilling it from the geo index in the background when it runs low.
- Swipe: the swipe service records the swipe and, for a like, checks whether the other user already liked back.
- Match: on a mutual like, a match is created once and both users are notified.
Deep Dives
1. Finding nearby candidates
Index active profiles in Elasticsearch (geo_point field + age, gender, last active) partitioned by region. A candidate query is: within the distance, matching both users' preferences (they're in my range *and* I'm in theirs), active recently, ordered by a score. See Finding What's Nearby.
Location updates are throttled (only when the user moves more than ~1 km or opens the app) to keep index writes manageable.
2. Precomputing stacks
- When a user's stack drops below ~20 profiles, a background job runs the candidate query, removes already-swiped profiles, ranks the rest, and appends ~200 to the user's Redis list.
- The feed endpoint just pops the next 50 — fast and cheap even at peak.
- Stacks go stale (people move, become inactive); give them a TTL of a few hours and filter out deactivated users on read.
Ranking: blend distance, recent activity, mutual-preference fit and a desirability or "likely to like back" score from an ML model. Showing profiles that are likely to swipe right back increases matches.
3. Never showing the same profile twice
The stack builder must exclude everyone a user has already swiped on — possibly tens of thousands of people.
- Querying the swipes table for every candidate is too slow.
- Keep a per-user Bloom filter of swiped user IDs (a few KB per user for 10k swipes at 1% false-positive rate) in Redis. A false positive only means skipping a profile occasionally, which is harmless. See Probabilistic Data Structures.
- Also exclude profiles already in the current stack, and remove a profile from the client's local stack the moment it's swiped.
4. Detecting matches consistently
When Alice likes Bob, check whether Bob already liked Alice. The race: both swipe at the same moment, each sees no like from the other, and no match is created.
Use a canonical key for the pair — pair = min(a,b) + ":" + max(a,b) — and record likes as fields on that key in a store with atomic operations (a Redis hash with a Lua script, or a DynamoDB item with a conditional update). The update that sets the second like sees the first and creates the match; the insert of the match itself is idempotent on the pair key. All swipes are also written durably to Cassandra (partitioned by swiper) for history and for rebuilding the Bloom filters.
[!WARNING] Don't forget the "both at once" case Interviewers often ask exactly this: "What if both users swipe right at the same millisecond?" The answer must involve a single atomic operation on a shared key, not two independent writes.
5. Notifications and chat
- On a match, publish a
MatchCreatedevent; the notification system sends push notifications to both users, and the chat service creates a conversation. - If the swiper is the second to like, show the match screen immediately from the swipe response.
6. Hot users and fairness
- Very popular profiles receive far more likes than others and appear in many stacks. Cap how often one profile appears in stacks per hour, both to spread attention and to limit load.
- Store incoming likes per user so the ranking can prioritize people who already liked you ("likes you" features) without extra queries.
What's expected at each level
- Mid-level: a geospatial query for candidates, swipe storage, and a basic match check.
- Senior: precomputed stacks, a Bloom filter or equivalent to avoid repeats, an atomic match check that handles simultaneous swipes, and location update throttling.
- Staff: ranking and fairness, scaling the geo index across regions, consistency between the durable swipe store and fast-path state, and how recommendations, matches and chat interact as separate services.