Free full guide

Tinder

Design Tinder — a location-based dating app with swipe feeds, mutual-match detection, and recommendation of nearby profiles.

00

Practice checkpoints

The interview rhythm stays compact, so the page can spend attention on the actual design decisions.

  1. 01
    Clarify scope
  2. 02
    Requirements + scale
  3. 03
    API + data model
  4. 04
    Draw architecture
  5. 05
    Deep dive
  6. 06
    Trade-off decision
01

Requirements that shape the design

Do not only state requirements. Ask for them. Each card pairs the design constraint with a clarification question you can say out loud before drawing the architecture.

Functional requirements

01What drives who shows up in my stack — preferences, distance, or both?

Users set preferences (age range, interests) and a maximum distance; the stack only contains candidates satisfying both.

02Is swiping one-at-a-time, and can a profile ever come back?

Users swipe yes/no one profile at a time, and a swiped profile never reappears — repeat profiles read as broken.

03What happens at the moment of a mutual yes?

Both users get the match notification immediately — the moment the second yes lands, not minutes later.

04Can a user undo a left swipe (rewind)?

Within a short window, yes — and the rewound profile can appear in the stack again.

05What does unmatching do?

Unmatch removes the match and closes the chat as one action — a half-revoked match (chat alive, match gone) is a trust bug.

06A user changes preferences mid-session - what happens to their deck?

New preferences take effect immediately — the very next profile shown must satisfy the updated filters, not the old ones.

Out of scopePhoto upload pipeline · Messaging after a match · Premium features (super swipes, boosts)

Non-functional requirements

01If two people swipe yes on each other at nearly the same instant, is exactly one match guaranteed — never zero, never two?

Exactly one match, detected immediately — never zero, never two, no matter how close the timing.

02What swipe volume are we sizing for?

20M daily active users (DAU) × ~100 swipes ≈ 2B swipes/day — about 23K swipes per second on average.

03How fast must the stack appear?

Under 300 ms.

04How hard is the "never show twice" rule?

Hard: it must hold across sessions, devices, and reinstalls — showing a profile twice is worse than quietly skipping one candidate.

05What location data can other users ever see?

Only a coarse distance bucket ("~5 km away") — raw lat/lng never leaves the backend, in any payload.

Keep asking — the interview is a conversation

Real interviews probe far more than a tidy list. These are the scope questions that separate candidates who interrogate the problem from those who recite it.

  • How long must swipe history stay around — forever, or can swipes from years ago expire?
  • Are bot accounts and fake-profile swipes in scope, or does an upstream trust-and-safety system filter them?
  • If someone blocks or reports a user, must the two disappear from each other's stacks immediately?
  • Do any markets impose data-residency rules — does a user's location data have to stay in-region?
  • Are super-likes / who-liked-me in scope? They change read patterns.
02

Numbers that force architecture decisions

Treat every estimate as a pressure that justifies a component: cache, queue, partition, replica, worker pool, or fallback path.

01

Swipe write rate

20M daily active users (DAU) × ~100 swipes/day2B ÷ 86,400 s ≈ 23K swipes/s average, evenings ×3-5 ≈ 100K/s peak

Swipes need a write-optimized store; the mutual check must stay O(1) per swipe at this rate.

02

Seen-profile memory

Assume a heavy user swipes ~50K profiles over a lifetimeBloom filter at 1% false positives ≈ 10 bits/entry → 50K × 10 b ≈ 62 KB per user

The entire "never show twice" guard fits in kilobytes per user — the exact swipe history stays on disk.

03

Deck precompute cost

Assume a deck of ~200 candidates per active user, refreshed when low20M users × 200 IDs × 8 B ≈ 32 GB

Precomputed decks for every active user fit in one cache tier — that is what makes <300 ms feasible.

04

Why not query live

A dense city can hold roughly 1% of 20M DAU inside one max-distance circle — about 200K users. Each one still needs geo, age, preference, and not-seen filtering.200K candidates × ~1-2 µs each to intersect filters and check the seen list ≈ 200-400 ms per request — the entire 300 ms budget burned before ranking even starts

The feed budget rules out running the live query on the request path. Instead, precompute the deck and top it up in the background.

05

Match check cost

Every yes-swipe checks the reverse direction1 atomic read-modify-write per swipe ≈ sub-ms in memory

Keeping the pair’s swipe state co-located (one key) is what keeps the check one operation.

Decision example

The numbers

A hundred thousand swipes a second at peak. The one moment that cannot go wrong is two people saying yes to each other at the same time.

My choice

I would store each pair’s swipe state under one key — the two user IDs, smaller first. Then I record the swipe and check for the reverse swipe as one atomic operation. A Lua script in Redis does that read-modify-write in a single step, and the durable copy is written to the swipe store behind it. Decks are precomputed per user and topped up by a background geo query. Each user’s seen profiles live in a Bloom filter, so "never show twice" costs kilobytes, not a history scan.

Avoid

What I would NOT do: record the swipe and check the reverse in two separate steps. Two simultaneous yes-swipes each miss the other, and the match never fires. That check-then-act gap is the same race that double-sells concert seats — the check and the write must be one operation. I also would not build the feed as a live geo query per request, because that burns the whole 300 ms budget on index lookups.

Change if

If Redis becomes the bottleneck, or cluster failover gets too complex, I would move the atomicity into the storage layer. A compound partition key (smaller_id:larger_id) makes both swipes land in one partition. There, a lightweight transaction covers the pair — the database’s own compare-and-set, slower than Redis but built in. It costs more per swipe, but saves one moving system.

03

Architecture path

One complete picture first, then each path as its own diagram — the write path and the read path carry different traffic and justify different components.

Complete picture

Overview — every component

ClientAPI GatewaySwipe ServicePair State(Redis, atomic)Swipe Store(durable)MatchNotificationsmutual yes → both usersDeck Service +Cacheprecomputed feedBloom Filters(seen)never show twice

The swipe path is the consistency-critical spine; the feed path is read-optimized and precomputed. Dashed = maintained asynchronously from durable swipes, rebuildable after cache loss.

Path 1

Swipe path — record and match atomically

ClientSwipe ServicePair Key (Lua,one op)Swipe StoreMatchNotification

One atomic operation records the swipe and checks the reverse. The durable write follows. If a Redis node is lost, it replays from the durable store and loses at most the last few unflushed swipes — an accepted trade for swipe-path speed.

Path 2

Feed path — precomputed deck, topped up

ClientDeck CacheDeck ServiceGeo + PreferenceQueryBloom Filter(exclude seen)

The request path only reads the deck. When it runs low, the background query refills it, excluding seen profiles via the Bloom filter — false positives skip a candidate, never repeat one.

04

API and data model

Before optimizing, make the contract inspectable: endpoints, entities, ownership, retries, and state.

POST/profile

req{ age_min, age_max, distance_km, interested_in }

res200 profile

Identity from the session token. Preference changes invalidate the precomputed deck.

GET/feed

res200 User[] (next slice of the deck)

Serves the precomputed deck; when it runs low, a fresh geo+preference query tops it up. Location comes from the session context, not query parameters.

POST/swipe/{target_user_id}

req{ decision: yes | no }

res200 { matched: boolean } — matched:true fires both notifications

The atomic step: record the swipe AND check the reverse swipe as one operation.

Core entities

User

user_id (PK) · profile · preferences · geo_cell (coarse)

Location is stored as a coarse geo cell for matching; precise coordinates are never served to other clients.

Swipe

swiping_user · target_user · decision: yes/no · swiped_at

Written durably to the swipe store; also folded into the swiper’s Bloom filter of seen profiles.

Match

match_id (PK) · user_a · user_b · matched_at

05

Deep dive directions

Pick one lane for the final third of the interview. Each lane gives you the topic, the interviewer question it should answer, and the failure mode to avoid.

Focus

The simultaneous yes

Ask

Two users swipe yes on each other in the same 10 ms. Walk the exact operations that produce one match, not zero.

Answer

Make it one atomic operation. Store both users' swipe state under a single key, the two IDs sorted smaller first. Record the swipe and check for the reverse swipe in one Redis Lua script. The second swipe always sees the first, so exactly one match fires.

Avoid

Read-then-write in two steps — both swipes miss each other and the match silently never happens.

Focus

Never show twice

Ask

How do you exclude 50,000 already-swiped profiles from every deck refill without scanning history?

Answer

Use a per-user Bloom filter of every profile they've swiped. At 50K entries that's about 62 KB. Screen each deck candidate against it; a false positive just skips a candidate and never repeats one. A Bloom filter can't unlearn, so keep the last few swipes in a small exact buffer — that's what powers rewind.

Avoid

Treating Bloom false positives as a bug — skipping one candidate is the designed cost; repeating one is the failure.

Focus

The deck runs dry

Ask

An active user swipes through their whole precomputed deck. What refills it, how fresh is it, and what latency do they see?

Answer

Answer every request from the precomputed deck cache. Refills fire asynchronously at a low-water mark, so a background geo-and-preference query rebuilds the 200-candidate deck before the user hits bottom. That precompute is what keeps a live multi-filter query out of the 300 ms path. A preference change drops the deck and refills the same way.

Avoid

Refilling synchronously on the empty-deck request — the 300 ms budget is gone before the query starts.

Focus

Location without leaking it

Ask

Matching needs distance, users must never see coordinates. Where does precision get dropped, and what does the API actually return?

Answer

Keep precise coordinates server-side only, for candidate selection. The backend computes the distance and rounds it into a coarse bucket like "~5 km away" before it goes into any response. No payload ever carries lat/lng in any field. Rounding on the client is theater — the payload itself is the leak.

Avoid

Sending raw lat/lng to clients and rounding in the UI — the payload is the leak.

Focus

Everyone swipes the same person

Ask

A very popular profile appears in millions of decks. What hot-spots first, and how do you keep exposure balanced?

Answer

The profile's pair keys and swipe partitions hot-spot first, so shard or replicate that state. Then give the profile an exposure budget: a cap on how many live decks it can occupy at once, refilled as swipes drain it. That way one popular profile can't fill every nearby deck and starve everyone else of views.

Avoid

Ignoring exposure skew — without caps, popular profiles dominate every deck and engagement collapses.

Ready to practice?

Talk through Tinder out loud and get AI scoring on the explanation.

Practice this with AI →