Design Tinder — swipe, match, and nearby stacks

Design Tinder

Junior-friendly Tinder walkthrough: swipe stacks with geo filters, atomic match detection (lost-match race → Redis Lua / same-partition Cassandra), cached+ES feeds, bloom filters to avoid re-shows, and interview bars by level.

Understanding the problem

Tinder (think a swipe-first dating app) is a consistency + geo-query interview. Pair with Contention, Proximity search, Cassandra, and Redis.

Two right swipes checked atomically become a match notification.
Two rights in one atomic check — notify both before true love is lost.
01Stack

Geo + prefs feed.

02Swipe

Left / right.

03Match

Atomic + notify.

04Dedup

Never re-show.

Functional requirements

Non-functional requirements

Core entities

User, Swipe, Match, Stack entities.
Swipe is the write path; Match is the reciprocal outcome; Stack is the read path.
  • User — profile, preferences, location.
  • Swipe — from → to, direction (left/right).
  • Match — unordered pair after mutual right.
  • Stack — ordered candidate IDs for a session (often cached).

API

GET /stack?limit= -> Profile[]
// geo + prefs applied server-side

POST /swipes
{ "toUserId": "…", "direction": "left"|"right" }
-> { matched: boolean, matchId?: string }

GET /matches -> Match[]

High-level design

Build FR by FR: profile → stack → swipe → match notify. Profile Service owns preferences; Swipe Service owns Cassandra swipes + push on match.

Client through gateway to Profile Service and Profile DB.
Create profile — prefs and max distance.
getStack and setProfile via Profile Service.
View a stack of potential matches.
Swipe Service and Profile Service with Cassandra and Profile DB.
Swipe left/right one-by-one — Swipe DB (Cassandra) + Profile DB.
Swipe Service pings push notification service on mutual match.
Mutual swipe → ping APNS/FCM → push to client.

Deep dives next: atomic match detection, fast stacks (ES + cache), and never re-showing swiped profiles.

Deep dive — consistent matches

Without atomicity: A and B both swipe right, each sees "no inverse yet," both rows get saved, nobody gets a match notification.

Concurrent swipes both miss the inverse and lose the match.
The lost-match race.

Bad — poll for matches

Misses the "immediate notify" NFR and burns DB QPS.

OK — transactions… but where?

Cassandra LWTs are single-partition only. At 2B swipes/day you can't put everything in one partition — so put each user pair in one partition.

Cassandra table keyed by sorted user_pair for single-partition atomicity.
Partition key min(id):max(id) — A→B and B→A land together.
def get_user_pair(a, b):
    x, y = sorted([a, b])
    return f"{x}:{y}"  # same partition for both directions

Great — Redis Lua + Cassandra durability

Redis hash per pair; Lua HSET own swipe + HGET inverse atomically. Match if both right. Flush/expire into Cassandra for history. Ops: cluster failover, aggressive TTL on Redis (lose only very recent match detection — user can re-swipe).

Swipe Service with Redis for atomic ops, Cassandra, Profile Service, and push notifications.
Redis for Atomic Operations — Cassandra for durable swipes.

Deep dive — low-latency stacks

Open app → swipe immediately. A multi-filter SQL scan won't cut it.

Slow SQL stack query on users table.
Naive query — age + interest + lat/long bounds.

Good — Elasticsearch / OpenSearch

Index prefs + geo. CDC from users DB (batch if write-heavy). See Elasticsearch and Proximity search.

Profile Service fetchStack via Elasticsearch with CDC from Profile DB.
Use of Indexed Databases for Real-Time Querying.

Better — precompute stack cache

Cron warms Stack Cache from Profile DB so getStack() is instant on app open.

Cron precomputing Stack Cache from Profile DB.
Pre-computation and Caching.

Great — cache + ES hybrid

Serve Stack Cache first; refill from Elasticsearch when the deck runs low so the stack feels infinite. Stale feeds: short TTL (<1h), recompute on filter/location change, warm only active users — tunable knobs without redesign.

Stack Cache plus Elasticsearch with Cron and CDC.
Combination of Pre-computation and Indexed Database.

Deep dive — never re-show swipes

  • OK: Feed builder queries swipe partition by swiping_user_id and filters — risks replica lag under AP; expensive for huge histories.
  • Better: Client keeps K recent swipes; filters while a new stack loads (assume one primary device).
  • Great: For huge histories, a bloom filter — no false negatives (never re-show a real swipe); rare false positives skip a few profiles (tunable).
Cron checks past swipes; Stack Cache and Elasticsearch for fetchStack.
Cache + DB Query + Contains Check.

For huge swipe histories, upgrade the swipe cache to a bloom filter — no false negatives (never re-show a real swipe); rare false positives skip a few profiles (tunable).

Final design

Final design with push, swipe bloom cache, Cassandra, Profile DB, Stack Cache, Elasticsearch, Cron, CDC.
Final Design — push notify · Redis/bloom swipe cache · Cassandra · stack cache + ES + CDC.

Cost and performance levers

What interviewers expect by level

Architecture by level

Three progressive sketches — beginner spine, mid optimizations, pro production depth. Use the matching diagram in the interview; say the tradeoff out loud.

Beginner Tinder: stack, swipe, and match APIs on a database.
Beginner architecture

Pros: Clear dating loop with geo filters and a re-show avoidance story. Cons: Concurrent mutual rights without atomicity lose match notifies; naive SQL stacks miss latency NFRs.

Mid Tinder: Redis Lua match detection and ES/cache stacks.
Mid architecture

Pros: Atomic pair-key matches; cache-first stacks with ES refill; stale-cache called out. Cons: Redis ops + ES CDC lag; must still explain Cassandra/history and client dedup.

Pro Tinder: pair partitions, Redis+Cassandra, bloom filters, tunable TTLs.
Pro architecture

Pros: Production dials (TTL, bloom FP%, warm set) and failure modes (Redis failover, hot pairs). Cons: Easy to over-build — lead with the race and stack p99 before bloom filters.

Interview takeaway

Mid: working dating loop. Senior: atomic matches + fast stacks. Staff: races, indexes, and tunable dedup.

Wrapping up

Tinder interviews reward pair-key atomicity, geo-aware stack plumbing, and honest dedup — with tunable TTLs and bloom error rates as ops dials.

Related: Delivery framework · Contention · Proximity search · Redis · Cassandra · Elasticsearch · Design Gopuff · Design Instagram · Interview bars by level.

← Lattice