Tinder's design question is dominated by one uncomfortable number: **swipes are enormous and almost all of them are worthless.** Billions of writes a day, of which a tiny fraction ever matter. Everything in the design follows from that asymmetry.
Requirements
Functional
- A deck of nearby, eligible profiles to swipe through.
- Swipe left/right; a mutual right-swipe creates a match.
- Chat after matching.
- Filters: distance, age, gender.
Non-functional
- The deck never runs out and never stalls — swiping must feel instant.
- Match notification within seconds.
- Never show the same profile twice.
75M monthly users, 10M daily
1.6B swipes/day ≈ 20,000 writes/second (peaks far higher)
Matches: roughly 1% of swipes → ~10M/day
Photo serving dominates bandwidthThe swipe store
The design decision people get wrong. A swipe row is written once and then queried in exactly one way: "has A already swiped B?"
swipes
partition key: swiper_id
clustering key: swipee_id
value: direction, timestampNo joins, no aggregation, no range scans. It is a pure key-value workload at very high write
volume, which is precisely what Cassandra or DynamoDB are for. Partitioning by swiper_id
makes both the write and the lookup single-partition.
For the "have I seen this profile" filter, storing and querying billions of rows per user is overkill — a Bloom filter per user answers it in constant memory, and a false positive merely means occasionally skipping someone, which is invisible.
Building the deck
Generating candidates on demand, per swipe, would put a geospatial query and a ranking model on the critical path. Instead, precompute a deck:
Background job, per active user:
1. Geospatial query: users within the distance filter
(S2/H3 cells, as in Uber — see the geospatial pattern)
2. Filter: age, gender, active recently, not already swiped,
not blocked, not already matched
3. Rank: attractiveness/engagement model, activity recency, reciprocal
likelihood (would they swipe back?)
4. Store the top ~100 ids in Redis as this user's deckThe client fetches a batch of 10–20 and prefetches photos, so swiping runs entirely from a local buffer. The deck is refilled asynchronously as it drains. Nothing user-facing waits on the model.
Ranking by reciprocal likelihood is the product insight worth mentioning: showing you
people who would also swipe right on you produces more matches than showing you the most desirable people on the platform, who will never match with most users.
Detecting a match
When A swipes right on B, the system must check whether B has already swiped right on A — and two simultaneous swipes must not create two matches or miss one.
1. Write A→B (right)
2. Read B→A
3. If it exists and is a right swipe → create the matchThe race: A and B swipe at the same instant, each reads before the other writes, neither sees a match. Two fixes:
- A canonical match key. Order the pair —
match_id = (min(a,b), max(a,b))— and
insert with a uniqueness constraint. Both racing requests attempt the same row; one wins, the other's conditional write fails, and exactly one match exists.
- Route both swipes through the same partition by hashing the ordered pair, so the two
operations are serialised by the store itself.
The canonical key is the cleaner answer, and it also makes the match idempotent under retries.
Once created, the match writes a conversation and pushes a notification to both users. Chat is a standard messaging system scoped to matched pairs — notably, one that cannot be initiated by a stranger, which removes most of the abuse surface.
Photos
Photos are the bandwidth cost and the first impression. Upload to blob storage, generate renditions, serve via CDN — the same pipeline as Instagram. Because the client prefetches the next several profiles, image delivery must be fast enough to stay ahead of the swiping, which in practice means small renditions and aggressive edge caching.
Follow-ups to expect
- Deck staleness — a precomputed deck ages: the user moves, or candidates become
inactive. Refresh on significant location change and expire decks after a few hours.
- Cold start — a new user has no signals; show broadly popular profiles and learn fast.
- Popular profiles — a small number of users receive most right-swipes; cap exposure to
keep their queue manageable and to give others visibility.
- Undo and rewind — a premium feature, so swipes must be reversible, which argues
against treating the swipe log as strictly immutable.
- Fake accounts and safety — photo verification, reporting, blocking, and ensuring a
blocked user never reappears in a deck.
- Location privacy — show distance buckets, never coordinates; exact distances can be
trilaterated.
Track this problem on the System Design sheet.
Frequently asked
How do you store billions of swipes efficiently?
As a wide-column key-value table partitioned by swiper id and clustered by swipee id, since the only query is 'has A already swiped B'. There are no joins, aggregations or range scans, so Cassandra or DynamoDB suit the very high write volume. A Bloom filter per user answers the 'already seen' check in constant memory instead of scanning rows.
How do you detect a mutual match without a race condition?
Use a canonical match key built from the ordered user pair — (min(a,b), max(a,b)) — and insert it with a uniqueness constraint. If both users swipe right simultaneously, both requests attempt the same row; one succeeds and the other's conditional write fails, so exactly one match is created. It also makes the operation idempotent under retries.
Why precompute the swipe deck instead of generating it live?
Because a geospatial query plus a ranking model on every swipe would put hundreds of milliseconds on the critical path of the app's core interaction. A background job produces a ranked deck of around a hundred candidates into Redis, the client fetches and prefetches a batch, and refills happen asynchronously — so swiping always runs from a local buffer.