Design Tinder
Run this for someone else. You hold the answers; they do not. Read the prompt, keep the clock, and use the probes below when an answer is thin. Do not show them this page.
Open with this
Show each person a deck of nearby profiles that fit both people’s preferences, never repeat one, take 2 billion swipes a day, and announce a match the instant two people like each other. Take a couple of minutes on requirements, then we will do some numbers, then the design. I will interrupt to keep us moving.
The clock
- 4 min: functional requirements and scope
- 4 min: non-functional requirements, with numbers
- 5 min: back-of-envelope estimates
- 16 min: high-level design and one or two flows
- 16 min: deep dives and the close
Move them on out loud when a section overruns. The commonest failure is spending twenty minutes on requirements and never reaching a deep dive, and preventing that is your job as much as theirs.
Requirements · 8 min
Listen for: a scoped set of capabilities, an explicit out-of-scope list, and numeric targets rather than adjectives. Prompt with “what are you not building?” if they never scope, and “what number would make that requirement real?” if they say “fast” or “highly available”.
Functional (6)
- Profile: Photos, bio, age, gender and preferences (who they want to see, age range, maximum distance).
- Get a deck: A stack of profiles near the user that match their preferences and whose preferences the user also matches. Never someone already swiped.
- Swipe: Left (pass) or right (like), recorded permanently. A right swipe on someone who already liked you is a match.
- Matches: Both people are told within seconds; the match appears in both match lists and opens a chat.
- Location updates: The user’s location refreshes when the app opens or they move, so decks reflect where they are now.
- Out of scope: Chat after the match (see Design WhatsApp), payments and boosts, photo moderation beyond a hook, and safety reporting.
Non-functional (6)
- Swipe volume (~2 B swipes/day · ~70 k/s peak): Every swipe is a write, and each one must be recorded before the next card loads.
- Deck latency (next card instantly; refill p99 < 300 ms): The app keeps a few cards buffered and refills in the background; a blank card is the failure users notice.
- Match latency (both notified within seconds): This is the requirement that drives the hardest trade-off: the match check must be synchronous and correct on the swipe path, at 70 k swipes a second, even when two people swipe on each other at the same instant.
- Never repeat: Showing someone you already passed on feels broken. The seen check runs on every deck build, across years of swipes.
- Locality (most queries hit one geo shard): Candidates are near the user by definition, so the index is split by geography, not by user id.
- Privacy: Exact locations are never exposed: distances are rounded and positions fuzzed.
Estimates · 5 min
Ask for two or three numbers, not all of them. What matters is whether they state assumptions, round sensibly, and say what the number implies. Push once with “where did that come from?”
The numbers (6)
- Swipes per second: ~23 k avg · ~70 k peak. 25 M DAU × 80 swipes a day = 2 B/day ÷ 86 400 ≈ 23 k/s; evenings run about 3× ≈ 70 k/s.
- Matches per second: ~300. 26 M matches a day ÷ 86 400 ≈ 300/s. Each needs two notifications and two match-list writes.
- Deck builds per second: ~3 k. Decks of 100 profiles; 80 swipes a day means about one refill per active user a day, plus app opens: 25 M × 10 builds a day ÷ 86 400 ≈ 3 k/s. Each is a geo query plus ranking.
- Swipe storage per year: ~20 TB. 2 B swipes × ~30 B (swiper, target, direction, time) = 60 GB a day × 365 ≈ 22 TB a year, before replication. Kept long-term because swipes are never shown again.
- Seen filter per user: ~12 KB. A heavy user has swiped ~10 k profiles. A Bloom filter at 10 bits per entry is 100 k bits ≈ 12.5 KB with ~1 % false positives, small enough to load on every deck build.
- Candidates in a dense city: ~50 k. Within 10 km of central London, matching basic age and gender preferences, there can be 50 k active profiles. The deck needs 100: candidate retrieval must cap and pre-score, not rank everyone.
High-level design · 16 min
Let them draw. Interrupt only to ask what backs a component or what a box actually does. Then pick one flow below and ask them to walk it end to end.
Components (15)
- Mobile app: Shows cards from a local buffer, sends swipes as they happen (batched briefly when offline), refills the deck in the background, and receives match notifications.
- API gateway: Authentication, per-user swipe rate limits (bots swipe right on everyone), and routing.
- Profile service: Profiles, photos (via CDN) and preferences. Profile and location changes are published so the candidate index stays current.
- Profile store: User profiles and preferences, sharded by user id. The source of truth the candidate index is built from.
- Recommendation service: Builds a deck: queries the user’s geo shard for candidates within distance and preferences (both ways), removes anyone already swiped, ranks, and caches about 100 profiles.
- Candidate index (Elasticsearch · geo-sharded): One document per active user: location, age, gender, preferences and activity, in shards that each cover a geographic area sized by population, so a deck query usually hits one shard.
- Ranking features: Batch and streaming jobs that compute signals for ranking (activity, response rates, likely mutual interest) from swipe events, and write them into the candidate index.
- Deck cache (per user, ~100 ids): Each user’s next ~100 recommended profile ids. The app takes 20 at a time; when the cache runs low, a rebuild runs in the background.
- Swipe service: Records each swipe idempotently, updates the user’s seen filter, and on a right swipe checks synchronously whether the other person already liked them.
- Seen filters (Bloom filter per user): A compact filter of every profile a user has swiped, checked on every deck build. False positives (rarely skipping someone new) are fine; false negatives are impossible.
- Likes store (Cassandra · key = (target, swiper)): Every right swipe stored by the person who was liked, so when A likes B, "has B liked A?" is one key lookup on A’s partition. Left swipes are kept separately for history and the seen filter.
- Match service: Creates a match exactly once per pair with a deterministic id, adds it to both match lists, and triggers notifications.
- Matches (key = (min_id, max_id)): One row per matched pair plus a per-user match list. The pair key is the same whichever person swiped last, which is what makes concurrent matches safe.
- Notifications (WebSocket + push): Tells both people "It’s a match!": over the open connection if the app is in use, through a push notification otherwise.
- Swipe events (Kafka): Every swipe and match, for ranking features, analytics, abuse detection and rebuilding the seen filters if needed.
Flows to ask them to walk (5)
- Open the app and get a deck: Cards come from a precomputed per-user deck; building it is a geo query on one shard, a seen filter, and a ranking pass.
- The app asks for the next cards; the deck cache returns 20.
- If fewer than ~30 remain, a background rebuild queries the user’s geo shard.
- Candidates the user has already swiped are removed using their seen filter.
- The rest are ranked, the top 100 cached, and the first cards hydrated.
- Swipe right and match: The write path: record the like once, check the reverse like with a single key lookup, and create the match exactly once.
- A swipes right on B.
- The swipe service stores the like under B and checks whether B already liked A.
- B is added to A’s seen filter, and the swipe goes on the event stream.
- B had liked A, so the match service creates the match.
- Both people are notified.
- Saturday 9 p.m. in a big city: The scale case: swipes hit 70 k/s, deck rebuilds spike in dense shards, and the busiest geo shards carry far more than their share.
- Swipes surge; the likes store absorbs them across many partitions.
- Analytics and ranking work is deferred to the event stream.
- Deck rebuilds concentrate on the shards for the busiest cities.
- Decks are refilled early, before users run out.
- Two people swipe right on each other at the same moment: The correctness case: both swipes see "no reverse like yet" or both see "yes". The pair key makes either outcome end in exactly one match.
- A and B swipe right on each other within milliseconds.
- If neither sees the other, both likes are stored and no match is created yet.
- If both see each other, both call the match service.
- If a notification is lost, the match list fixes it.
- Profiles and locations keep the index fresh: The background path: the candidate index follows profile and location changes, and inactive users drop out of decks.
- A user opens the app in a new city; the app reports a coarse location.
- The profile service moves the user’s document to the shard for the new area.
- Ranking signals are refreshed from swipe events.
Deep dives · 16 min
Pick two. Ask the headline question, let them answer, then use the follow-ups. The follow-ups are where the level gets decided, so leave time for at least three of them.
Building the deck
Ask: How do you pick 100 profiles for someone from 75 million, fast, and so that both sides are likely interested?
Good answers name: Geo-sharded search index with two-sided filters, seen-filter removal, ranking, deck of ~100 cached per user, Query candidates on every card, Precompute decks for every user nightly.
Our pick: Profiles live in an Elasticsearch-style index sharded by geography. A deck build queries the user’s shard with both-way filters (distance, age and gender each way, active within a week), sorted by a cheap pre-score and capped at a few thousand; removes everyone in the user’s seen filter; ranks the rest with a model predicting mutual interest, adjusted for fairness of exposure; and caches the top 100 ids per user. The app shows 20 at a time and refills when 30 remain. Changes to the user’s location or preferences invalidate the cached deck.
- A new user signs up. Who sees them, and how do they get good recommendations?
New users have no swipe history, so ranking leans on profile completeness, activity and broad popularity, and they get a temporary exposure boost so others see them and data accumulates quickly. Their own deck starts from simple filters and improves as they swipe. - What if a user’s shard has run out of people to show?
Widen the distance step by step (25 km to 50 to 100), possibly into neighbouring shards, and tell the user they have seen everyone nearby. Rural users hit this often, so the expansion is a normal path. - Why rank for mutual interest instead of attractiveness?
Showing everyone the most popular profiles concentrates likes on a few people who cannot respond to them all, and most users get no matches. Ranking for the chance that both swipe right spreads attention and produces more matches overall. - How do you evaluate a ranking change?
By matches and conversations started per active user, not swipes, and by fairness of exposure (how many users get at least some likes). Run it as an A/B test by region, since the two sides of the market affect each other. - Should decks be refreshed when someone in them deletes their account?
Hydration skips missing or inactive profiles when cards are served, so a stale id in the cached deck costs nothing visible. Rebuilding every deck that contains a departed user would be far more work than filtering at serve time.
Never showing someone twice
Ask: A user has swiped 10,000 profiles over two years. How do you make sure none of them reappear?
Good answers name: A Bloom filter per user, updated on every swipe, rebuilt from the swipe log if lost, Exact set of swiped ids per user, Query the swipe table for each candidate.
Our pick: Each user has a Bloom filter sized for their swipe count (about 10 bits per swipe for ~1 % false positives), stored in a Redis cluster keyed by user id. The swipe service adds every swiped id as it records the swipe; the recommendation service fetches the filter once per deck build and tests each candidate in memory. When a user’s swipes outgrow the filter’s size, a new, larger filter is built from the swipe history. The swipe history in the event log is the source of truth, so a lost filter is rebuilt rather than trusted empty.
- The Redis node holding seen filters fails. What do users see?
Without a filter, decks would show people already swiped, which is worse than a short delay. The recommendation service treats a missing filter as "rebuild first": it reconstructs it from the swipe history (one partition read) before building the deck, or serves the remaining cached deck meanwhile. - Should a pass be permanent?
Product decision. Some apps re-show passed profiles after months because preferences change. That fits the design: keep left swipes with timestamps and build the filter from recent passes plus all likes. - How do you size a filter for a user who keeps swiping?
Start small (sized for, say, 1,000 entries) and when the count passes the design capacity, build a larger filter from the swipe history and swap it in. Scalable Bloom filters do this automatically by chaining filters of growing size. - Why not check the seen filter in the index query itself?
The index would need each user's swipe list as a query filter, thousands of ids per query, which is slow and huge. Filtering a few thousand candidates in the service against a 12 KB filter is far cheaper.
Detecting a match exactly once
Ask: How do you know, at the moment of a right swipe, whether the other person already liked you, and avoid creating two matches?
Good answers name: Store likes by target; write own like then read reverse like; create match with a deterministic pair key and conditional insert; sweep for missed pairs, Lock the pair while swiping, Detect matches only asynchronously from the stream.
Our pick: A right swipe writes (target, swiper) into a likes table partitioned by target, then looks up whether (target = swiper, swiper = target) exists. If it does, the match service inserts a match keyed by (smaller id, larger id) with IF NOT EXISTS, adds it to both users’ match lists, and notifies both. Because each side writes before reading, at least one side sees the other’s like whenever the writes complete before the reads; for the remaining sliver, a stream job joins recent likes in both directions and creates any missing match within seconds. The pair key makes every path converge on one match.
- A unmatches B. What has to change?
Delete the match row and both list entries, and keep a block record so neither appears in the other’s deck again. The likes stay as history but must not recreate the match, so the match job checks for a block before inserting. - Why is a like stored by the person who was liked?
Because the question asked on every right swipe is "has the person I just liked already liked me?", which is a read of my own likes-received partition. Storing by swiper would make it a search through the other person’s likes. - How do you handle a match notification for a user with the app closed?
Send a push notification and record the match in their list. When they open the app, the match list sync shows it even if the push never arrived, so the push is a convenience, never the only record. - Could you use the swipe stream alone to detect matches?
Yes, as a fallback and for the rare race, but not as the only path: the user who swipes second expects an instant answer. The synchronous check gives that; the stream makes it complete.
Sharding by geography
Ask: Why shard the candidate index by location instead of by user id, and how do you draw the shard boundaries?
Good answers name: S2 cells grouped into shards balanced by active users; a query covers one or a few shards, Shard by user id, Fixed grid of equal-area shards.
Our pick: Divide the world into S2 cells at a fixed level, measure active users per cell, and group contiguous cells into shards with roughly equal load, so dense cities span several shards and sparse regions share one. A small, cached mapping from cell to shard routes each document and each query; a deck query computes the cells covering its search radius and queries the shards that own them, usually one. Shard boundaries are recomputed periodically as usage changes, with documents moved in the background. This mirrors the geosharded recommendations design Tinder engineering described.
- A user is on the border between two shards. What happens?
The covering cells for their search radius belong to two shards, so the query goes to both and results are merged. It costs one extra query for users near borders, which is why shard boundaries are drawn through low-density areas where possible. - Why not use a geohash instead of S2?
Either works for dividing space. S2 cells are roughly equal in area everywhere on the sphere and have good covering algorithms for circles, while geohash cells distort near the poles and have awkward edges. The balancing idea is the same with both. - How do you rebalance shards without downtime?
Compute new boundaries, create the new shards, and dual-write documents for the affected cells to old and new shards while backfilling. Switch query routing per cell once the new shard is caught up, then delete from the old one. - Does the shard map need to be consistent everywhere instantly?
No. Routing with a slightly stale map sends a query to the old shard, which still has the data during a migration because of dual writes. Maps are versioned and refreshed every few seconds.
Taking 70,000 swipes a second
Ask: Every swipe is a write. What must happen synchronously, and what can wait?
Good answers name: Synchronous: idempotent like write, reverse-like lookup, seen-filter add. Asynchronous: everything else via Kafka, Everything asynchronous, Everything synchronous, including features and analytics.
Our pick: The app sends swipes immediately (and batches them while offline) with a client id for idempotency. The swipe service writes the like or pass to a wide-column store partitioned by target, adds the target to the seen filter, and for likes does the reverse lookup and match creation; then it publishes the swipe to Kafka. Feature computation, analytics and abuse detection consume the stream. A per-user rate limit at the gateway stops bots that like everyone, which would otherwise flood other people’s likes and distort ranking.
- How do you detect a bot that swipes right on everyone?
Its signature is obvious in the stream: a like rate near 100 %, at machine speed, with no conversations. Rate limits slow it; an abuse model on the event stream flags it and its likes are removed from other users’ likes partitions so they do not produce fake matches. - The app is offline for an hour and swiped 50 cards. What happens on reconnect?
It uploads the queued swipes in order with their client ids. Matches discovered are returned in the batch response. If any target became unavailable meanwhile (deleted account), the swipe is simply recorded and ignored. - What consistency does the likes store need?
Read-your-writes for the swiping user and a quorum read for the reverse-like check, so a like written a moment ago by the other person is visible. Eventual consistency elsewhere is fine, because the stream sweep repairs any match the check misses. - How long do you keep swipe history?
Likes as long as the account exists, because they are needed for matching and for the seen filter. Passes can expire after a policy period if the product allows people to reappear. Deleting an account deletes its swipes and removes it from others' likes.
Close · 5 min
Ask what breaks first at ten times the load, and what they would build next. Then give them your read: one thing that was strong, one thing that was missing, one thing to practise. Be specific; “good job” helps nobody.