System Design

Design a Dating App

Step 1 / 9

Learn system design by building a swipe-based dating app like Tinder or Hinge step by step.

The numbers to beatgeohash cellsinstead of a full distance scanBloom filtercheap "have I seen this person" checkO(cells)candidate generation cost, not O(all users)

Deep cut · 13:31

One swipe, and the row it collided with

The interactive walkthrough above lays out geospatial candidate generation, the swiped-exclusion set, the mutual-match race, precomputed ranked feeds, instant bidirectional blocking and async trust scoring. This film is told from inside the app on the phone in your hand, and carries one number along the top of every frame: how many rows were scanned to put this card on your screen. It opens at about 6,000,000 per swipe and ends at 0.

  • See the decision everything rests on: nothing is searched for at swipe time. A background job ranks a deck of about thirty before you open the app, so a swipe is a read of card seven off a list that already existed — which is the only way a card lands in under a hundred milliseconds against six million accounts nearby.
  • See where the obvious build actually dies: not on cost, on correctness. Two people swipe right in the same instant, both requests ask “does the other one already like me?”, both are answered before either like is written down, and the result is zero matches — with no error, nothing in a log, and two people who both said yes never told.
  • See the reveal the whole film is built toward: stop asking the question. Sort the two account numbers, put a unique constraint on the pair, and let the store decide — the second write bounces off the first and reads the row that is already there. Her like has been sitting in that table since Tuesday. Your swipe did not find it; it collided with it.
  • Take it into the interview: derive geohash cells, a Bloom-filter exclusion sketch, a deck ranked in advance, a uniqueness constraint on the sorted pair, chat gated on the match row, synchronous uncacheable blocking and quiet async trust scoring each from the number that demanded it — with what every one of them costs said out loud, including a candidate the sketch silently drops and two accounts that can never be stored apart.

In the interview room

How you’d open this design in an interview

Before any boxes: agree what it must do, pin the qualities that shape everything, then build — naming each trade-off as you make it. The walkthrough above is that exact order.

Functional requirements

What it must do — agree on these before drawing a single box.

  • Serve a deck: show a stream of nearby candidates the user hasn’t already swiped on.
  • Swipe & match: record each like; when both directions exist, create exactly one match.
  • Rank: order the deck by more than distance — recency, mutual signals, paid boosts.
  • Chat on match: open a messaging channel only once a match is confirmed.
  • Block & report: hide two users from each other immediately and bidirectionally.

Non-functional requirements

The qualities that shape the whole design — each one names the mechanism that buys it.

Deck lookup scales, and never repeats a face
Users are indexed by geohash cells so "nearby" is a few cell lookups, and a per-user swiped-exclusion set filters seen profiles before the deck is even built.
Exactly one match, even on a simultaneous swipe
A unique constraint on the unordered user pair plus an atomic conditional insert lets the database — not application timing — guarantee exactly one match row.
Ranking quality without paying per swipe
Ranked candidate batches are precomputed periodically and served until exhausted, so only the refresh touches the expensive multi-signal model.
No messaging a stranger
The chat service checks the matches store before allowing any message, reusing match-state as the single source of truth for who may talk.
Safety is the strictest path in the system
Blocks write synchronously to the authoritative store and every read path checks it, with no caching layer allowed to serve a stale "not blocked" answer.
Catch bots without slowing real users or tipping them off
A trust-scoring pipeline runs asynchronously off the swipe path and quietly shadow-restricts low-trust accounts, so it never blocks a swipe or reveals what tripped it.

The trade-offs you say out loud

Senior signal isn’t the boxes — it’s naming what you gave up and why it was the right price.

A geospatial index + swiped-exclusion setover a live full-table distance scan per swipe

Scanning distance for every nearby user on every swipe doesn’t scale past a small base, and with no memory of who’s been shown the same faces resurface. Geohash cells make "nearby" cheap; the exclusion set makes "no repeats" cheap.

A DB uniqueness constraint + atomic insertover a check-then-insert in application code

Two "does the reverse like exist?" checks can interleave before either write commits, creating zero matches or two. Pushing the invariant down to the database guarantees exactly one match row regardless of swipe timing.

Precomputed ranked batchesover computing ranking live on every swipe

Recomputing a multi-signal model at swipe-scale traffic is expensive for a benefit users can’t perceive between one swipe and the next. A batch computed every few minutes serves many swipes with imperceptible staleness.

Strongly-consistent synchronous blockingover eventual consistency like the feed

A few minutes where a blocked user can still see or message the blocker is a safety failure, not a minor inconsistency. Safety gets the strongest guarantee in the system even though it’s the rarest action.

Async, quiet trust scoringover instant, visible account restriction

Synchronous fraud checks would slow every swipe to catch a minority of bad actors, and a visibly-restricted account tells a sophisticated bot exactly which signal to change. Decoupling detection from the hot path — and enforcement from disclosure — matters as much as accuracy.

What this teaches

Learn system design by building a swipe-based dating app like Tinder or Hinge step by step. An interactive guide covering why a live full-table nearby scan doesn't scale, geospatial candidate generation with a swiped-exclusion set, the mutual-match race condition and how to guarantee exactly one match record, precomputed ranked feeds, instant bidirectional blocking as a safety-critical path, and an async trust-scoring pipeline for bot and fake-profile detection.

Key takeaways

  • A live full-table nearby scan doesn't scale and repeats candidates — fix both with a geospatial index plus a swiped-exclusion set.
  • A mutual match under concurrent swipes needs a database-enforced uniqueness constraint and an atomic conditional insert, guaranteeing exactly one match regardless of timing.
  • Ranking is precomputed in batches and served from cache — swipe volume is far too high to rank live per request.
  • Chat is gated on the matches store: no messaging without a confirmed match.
  • Blocking is deliberately the strictest-consistency path in the system — synchronous, bidirectional, checked on every read, no staleness tolerated.
  • Bot/fake-profile detection runs asynchronously and quietly, off the hot path, so it never slows real users or tips off adversaries.
  • Swipe limits are a simple token bucket per user per day, which also naturally supports paid tiers.

Concepts covered

  • What is a dating app, systems-wise?
  • Show everyone nearby, sorted by distance
  • Fast lookup, no repeats
  • A like is one-directional. A match needs two.
  • More than distance — but not computed live
  • No messaging a stranger
  • Safety gets the strongest guarantee in the system
  • Score behavior, quietly, off the hot path
  • Rate limits and the freemium wall

Design a Dating App — read the full walkthrough as text

the same steps, decisions & trade-offs, for reading, reference & search

The big idea

What is a dating app, systems-wise?

Millions of people, each wanting a fresh deck of nearby candidates they haven't seen before, each swipe needing to feel instant. Buried inside that simple feed are two genuinely hard problems: a mutual-match has to be detected exactly once even when both people act at the same instant, and safety actions (blocks, reports) need guarantees the rest of the system doesn't.

We'll build the candidate feed, the matching logic, and the safety path as three different consistency problems wearing one UI: the feed can be a little stale, the match has to be exactly right, and blocking has to be immediate. Getting each one the right level of "strict" is the whole design.

How to read this: Each step opens with a real design decision — make the call before I show you what ships. Watch the diagram grow, hover the boxes, and at the end race a simultaneous mutual swipe and fire a block to see which guarantees actually hold. Hit Begin.

Step 1 · The baseline

Show everyone nearby, sorted by distance

The simplest version: on every swipe, query the users table for everyone within N km, sorted by distance, and show the next one. What breaks first as this scales?

Design decision: A live "everyone within N km" query on every single swipe, at real scale. What breaks first?

The call: The live full-table geo scan gets slower and more expensive as the user base grows, and with no exclusion tracking, already-swiped or already-matched people reappear in the deck. — Scanning distance for every nearby user on every single swipe (there are far more swipes than any other action in this product) doesn't scale past a small user base, and without remembering who's already been shown, the same people resurface — an obviously broken experience.

A live full-scan-by-distance on every swipe doesn't scale, and without tracking who's already been shown, candidates repeat. You need both a fast geospatial lookup and a way to never re-show someone the user already swiped on.

Two different problems in one feed: "Find people near me" (a geospatial query) and "don't show me the same person twice" (an exclusion set) are separate concerns that both have to be solved before the naive version is even correct, let alone fast.

Step 2 · Geospatial index + exclusion set

Fast lookup, no repeats

Fix both problems from Step 1: make "nearby" fast at scale, and make "already seen" reliable.

Index users by geohash cells (or S2/geospatial index) so "nearby" becomes a handful of cell lookups instead of a distance calculation across the whole table — the same principle as the proximity-service pattern. Pair it with a per-user swiped-exclusion set (a Bloom filter or a fast set-membership store) so candidate generation filters out anyone already swiped on before the deck is even built, not after.

Index the query, remember the history: Geohashing turns "who is near this point" into a small number of bucket lookups by encoding location into a string prefix — nearby points share a prefix. The exclusion set is a separate, cheap membership check layered on top, so the two concerns (fast geo lookup, no repeats) stay independently solvable.

Step 3 · The mutual match

A like is one-directional. A match needs two.

A swipe-right just records "A likes B." A match only exists once BOTH directions are true. When B swipes right on A, the system has to check "does A already like B?" and if so, create exactly one match. What could go wrong under real concurrency?

Design decision: B's like triggers a check for A's existing like, then a match insert. Under concurrency, what's the failure mode?

The call: If A and B swipe right on each other at the exact same instant, both checks can run before either like commits — risking zero matches created (both see "no reverse like yet") or a duplicate match row, unless the match creation is atomic and idempotent. — This is a genuine race: both swipes' "does the reverse exist?" checks can interleave before either write lands, so a naive implementation can miss the match entirely or create it twice. The fix is a DATABASE-level guarantee — typically a unique constraint on the unordered (userA, userB) pair plus an atomic conditional insert — so the database itself, not application logic, decides there is exactly one match row no matter the timing.

Enforce a unique constraint on the unordered user pair in the matches table, and create matches via an atomic conditional insert (or an upsert) rather than a separate check-then-insert. Whichever swipe commits second simply hits the constraint, discovers the match already exists, and confirms it — guaranteeing exactly one match row regardless of how closely timed the two swipes are.

Let the database enforce the invariant: "Exactly one match per pair, no matter the race" is not something application-level timing checks can reliably guarantee — it's exactly what database uniqueness constraints exist for. Push the invariant down to the layer that can actually enforce it atomically.

Step 4 · Ranking the deck

More than distance — but not computed live

Distance alone is a weak signal — recency of activity, mutual interest signals, and paid "boosts" all matter for who shows up near the top of the deck. Should this ranking be computed fresh for every single swipe?

Design decision: Ranking depends on several signals and paid boosts. Compute it live on every swipe, or something else?

The call: Precompute ranked candidate batches periodically (or on meaningful signal change) and serve from that batch until it's exhausted or stale. — Just like calendar or hotel pricing, ranking doesn't need per-request freshness — a batch of, say, 50 ranked candidates computed every few minutes (or refreshed when the user's own activity changes meaningfully) serves many swipes cheaply, with a small, imperceptible staleness window.

Precompute ranked candidate batches periodically per user, incorporating recency, interaction signals, and boosts, and serve swipes from that batch until it's exhausted or a refresh triggers. Only a small fraction of traffic (the refresh itself) touches the expensive ranking computation.

Same caching principle, different domain: This is the identical trade-off as hotel pricing and calendar reminders: an expensive, multi-signal computation gets computed periodically and served from a cache/batch, because per-request freshness isn't worth its cost at this traffic volume.

Step 5 · Chat unlocks on match

No messaging a stranger

Once a match exists, both people should be able to message each other. Before a match, they shouldn't.

A confirmed row in the Likes/Matches store is the gate for opening a chat channel — the chat service checks match existence before allowing any message, and the channel itself is created (or lazily opened) the moment the match is confirmed. This reuses standard real-time messaging architecture (fan-out, presence, delivery) with one extra precondition layered on top: a match must exist first.

One store, two consumers: The matches table isn't just a record of "who matched" — it's the authorization check for an entirely different feature (chat). Keeping match-state as the single source of truth for that gate avoids duplicating "are these two allowed to talk" logic anywhere else.

Step 6 · Blocking is not eventually consistent

Safety gets the strongest guarantee in the system

One user blocks another. Should that propagate on the same "eventually, within a few minutes" timeline as the ranked candidate feed?

Design decision: A block needs to hide two people from each other. Same eventual-consistency tolerance as the recommendation feed?

The call: No — a block must take effect immediately and bidirectionally: hidden from each other's candidate deck, chat, and search, right away, everywhere. — Safety actions are the one place in this system where "eventually consistent" is unacceptable. A block writes synchronously to the authoritative store and every read path (candidate generation, chat, search) checks it before serving results — no caching layer is allowed to serve a stale "not blocked" answer.

Blocks and reports write synchronously to the authoritative safety store, and every read path that could expose the blocked pair to each other — candidate generation, chat, search — checks it before serving results, with no caching layer permitted to serve a stale answer here. This is deliberately the strictest consistency guarantee in the entire system, even though it's the rarest action.

Not every path deserves the same consistency model: The recommendation feed optimizes for scale and tolerates staleness because the cost of being wrong is low (you see a slightly outdated candidate). Safety optimizes for correctness and accepts extra cost because the cost of being wrong is high. A good system design explicitly chooses different guarantees for different paths — it doesn't apply one blanket policy everywhere.

Step 7 · Catching fake profiles and bots

Score behavior, quietly, off the hot path

Bots and fake profiles swipe right on everyone, reuse stolen photos, and cluster on the same devices. Detecting this needs to analyze behavior patterns across many accounts — expensive, and if a suspicious account is instantly and visibly restricted, sophisticated bots simply adapt and evade next time.

Run a trust-scoring pipeline entirely asynchronously, off the hot swipe path: it ingests signals (swipe velocity, image-hash reuse across "different" profiles, device/IP clustering) and produces a trust score per account over time. Low-trust accounts are quietly throttled or shadow-restricted in candidate generation — shown to fewer real users, not blocked outright and not told why — so the swipe path never waits on this analysis, and bad actors don't get a clear signal telling them exactly what tripped the detection.

Decouple detection from enforcement, and enforcement from disclosure: Fraud/trust systems generally work best asynchronous (don't slow down the real product for everyone to catch a minority of bad actors) and quiet (an adversary who can see exactly what got them flagged will simply avoid that specific signal next time). Both properties matter as much as the detection accuracy itself.

Step 8 · The sharp edges

Rate limits and the freemium wall

Most dating apps cap free swipes per day. How do you enforce "N swipes per 24 hours" cleanly, and combine everything built so far into one coherent system?

Enforce swipe limits with a token bucket per user per day-boundary — cheap to check on every swipe, refills on a schedule, and naturally supports paid tiers by simply configuring a larger (or unlimited) bucket. Combined with everything else: fast geo candidates with no repeats, an atomically-guaranteed single match per pair, precomputed ranking, chat gated on match state, safety enforced with the strictest consistency in the system, and bot detection running quietly in the background — that's a dating app that survives real concurrency, real abuse, and real scale.

Design for the unhappy path: Simultaneous mutual swipe → atomic conditional insert with a uniqueness constraint. A block → synchronous, strongest-consistency enforcement everywhere. A bot farm → async, quiet trust scoring. Each unhappy path gets the specific guarantee it actually needs, not a one-size-fits-all fix.

You did it

You just designed a dating app.

  • A live full-table nearby scan doesn't scale and repeats candidates — fix both with a geospatial index plus a swiped-exclusion set.
  • A mutual match under concurrent swipes needs a database-enforced uniqueness constraint and an atomic conditional insert, guaranteeing exactly one match regardless of timing.
  • Ranking is precomputed in batches and served from cache — swipe volume is far too high to rank live per request.
  • Chat is gated on the matches store: no messaging without a confirmed match.
  • Blocking is deliberately the strictest-consistency path in the system — synchronous, bidirectional, checked on every read, no staleness tolerated.
  • Bot/fake-profile detection runs asynchronously and quietly, off the hot path, so it never slows real users or tips off adversaries.
  • Swipe limits are a simple token bucket per user per day, which also naturally supports paid tiers.
built so two mutual right-swipes become exactly one match, never zero, never two — make the calls, race a double-swipe, run the gauntlet.
Finished this one? 0 / 65 System Designs done

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More System Designs