Design a Proximity Dating App Like Tinder
A proximity-based dating app (e.g., Tinder, Bumble, Hinge) matches users based on geographic proximity, mutual attraction, and age/gender preferences. Users swipe right to like or left to pass. When two users mutually like each other, the system triggers an immediate match notification and unlocks a direct messaging channel.
1. Understanding the Problem
Functional Requirements
- User Profile & Preferences: Users configure photos, bio, gender, sexual orientation, target age range, and maximum search distance (e.g. within 25 miles).
- Recommendation Deck: Generate a continuous deck of candidate profiles matching the user's discovery filters and location radius.
- Swipe Ingestion: Record user swipes (Right = Like, Left = Pass, Up = Super Like) with low latency.
- Mutual Match Detection: Detect instant mutual likes and notify both users in real-time (
"It's a Match!"). - Location Updates: Update user geographic coordinates periodically when the app is active.
Non-Functional Requirements
- Low Latency Swipe Processing: Swipes must be acknowledged in
< 50msso user swiping feels instantaneous. - Instant Match Notification: Match alert delivered to both clients in
< 1svia WebSockets / APNS. - High Throughput: Support up to 2 Billion swipes per day worldwide.
- Fair Recommendation Distribution: Candidate decks must be dynamically refreshed without showing the same profile repeatedly or showing inactive accounts.
Capacity Estimations & Sizing
- Daily Active Users (DAU): 50 Million users.
- Average Swipes per User: 40 swipes/day .
- Average Swipe QPS = 23,000 QPS (peaking at 60,000 QPS during evening hours).
- Match Rate: ~1% of right swipes result in a mutual match 10 Million matches/day (~115 matches/sec).
- Storage Calculation (5 Years):
- Swipe records: 2B swipes/day 365 5 = 3.65 Trillion swipes.
- Storing every swipe permanently in relational DB is cost-prohibitive.
- Stored as compact key-value pairs or partitioned Cassandra rows:
(swiper_id, target_id, decision, timestamp)32 bytes 116 TB storage over 5 years.
2. The Set Up
Defining the Core Entities
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β USER_PROFILE β
ββββββββββββββββββββ¬βββββββββββββββ¬βββββββββββββββββββββββ€
β user_id β UUID β PRIMARY KEY β
β name β VARCHAR(64) β NOT NULL β
β birth_date β DATE β Age calculation β
β gender β VARCHAR(16) β M / F / NON_BINARY β
β preference β VARCHAR(16) β Preferred gender β
β min_age β INT β Preference filter β
β max_age β INT β Preference filter β
β max_distance_km β INT β e.g. 50 km β
β last_location β GEOMETRY β Point (Lat, Lng) β
β last_active_at β TIMESTAMP β Activity filter β
ββββββββββββββββββββ΄βββββββββββββββ΄βββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β SWIPE β
ββββββββββββββββββββ¬βββββββββββββββ¬βββββββββββββββββββββββ€
β swiper_id β UUID β COMPOSITE PK, FK β
β target_id β UUID β COMPOSITE PK, FK β
β decision β CHAR(1) β 'L' (Like), 'P'(Pass)β
β created_at β TIMESTAMP β NOT NULL β
ββββββββββββββββββββ΄βββββββββββββββ΄βββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β MATCH β
ββββββββββββββββββββ¬βββββββββββββββ¬βββββββββββββββββββββββ€
β match_id β UUID β PRIMARY KEY β
β user_a_id β UUID β INDEX, FK (Lesser ID)β
β user_b_id β UUID β INDEX, FK (Greater IDβ
β created_at β TIMESTAMP β NOT NULL β
ββββββββββββββββββββ΄βββββββββββββββ΄βββββββββββββββββββββββ
The API Design
1. Ingest Swipe (Right or Left)β
POST /api/v1/swipes
Content-Type: application/json
Authorization: Bearer <jwt_token>
{
"target_user_id": "usr_78a10-9281",
"decision": "LIKE" // "LIKE" or "PASS"
}
Response (200 OK):
{
"status": "RECORDED",
"is_match": true,
"match_details": {
"match_id": "match_99018",
"matched_user": {
"user_id": "usr_78a10-9281",
"name": "Sarah",
"photos": ["https://cdn.tinder.com/photos/sarah_1.jpg"]
}
}
}
2. Fetch Recommendation Deckβ
GET /api/v1/recommendations?limit=20
Authorization: Bearer <jwt_token>
Response (200 OK):
{
"candidates": [
{
"user_id": "usr_10284",
"name": "Alex",
"age": 27,
"distance_km": 4.2,
"bio": "Coffee lover & mountaineer",
"photos": ["https://cdn.tinder.com/photos/alex_1.jpg"]
}
]
}
3. High-Level Design
Walkthrough of Core Flows
1. Swipe Ingestion & Instant Mutual Match Detectionβ
- User A swipes Right on User B
POST /api/v1/swipes. - The Swipe Service queries an in-memory Redis Like Cache to check if User B has already swiped right on User A:
- Check key:
HEXISTS likes:UserB UserA.
- Check key:
- If NO (Single Like):
- Records User A's like in Redis:
HSET likes:UserA UserB timestamp. - Asynchronously publishes the swipe event to Kafka to update persistent storage (Cassandra/DynamoDB) and machine learning ranking models.
- Returns
{is_match: false}immediately to User A in< 25ms.
- Records User A's like in Redis:
- If YES (Mutual Match!):
- Both users have liked each other!
- Creates a new
MATCHrecord in the relational database. - Publishes a
MatchCreatedEventto Kafka. - The Notification Service pushes real-time WebSocket alerts to both User A and User B concurrently.
- Returns
{is_match: true}in the HTTP response.
2. Recommendation Deck Generationβ
- When User A opens the app, the Recommendation Service:
- Determines User A's current Geohash / S2 Cell.
- Queries the Geospatial Index (Redis Geo / ElasticSearch) to retrieve active users within User A's search radius (e.g. 25 miles).
- Filters candidate IDs against User A's past swipe history (
SISMEMBER swiped:UserA candidate_id) to ensure users never see someone they already swiped on. - Filters by age and gender preferences.
- Ranks the remaining candidate pool using an ML desirability / compatibility score.
- Returns top 20 candidate profiles.
4. Potential Deep Dives & Bottlenecks
Deep Dive 1: Mutual Match Concurrency & Race Conditions
What happens if User A and User B swipe right on each other at the exact same millisecond across two different application servers?
User A swipes Right on B User B swipes Right on A
β β
βΌ βΌ
Check: Did B like A? (No) Check: Did A like B? (No)
β β
Write: A likes B Write: B likes A
β β
NO MATCH DETECTED! NO MATCH DETECTED!
β CATASTROPHIC BUG: A mutual match was created, but neither user was notified!
The Atomic Solution: Redis Distributed Lock / Lua Scriptβ
To eliminate this race condition, we order the evaluation using a deterministic key lock:
-- Lua script executed atomically on Redis
-- KEYS[1]: likes:UserA, KEYS[2]: likes:UserB
-- ARGV[1]: UserA_ID, ARGV[2]: UserB_ID
local b_liked_a = redis.call('HEXISTS', KEYS[2], ARGV[1])
if b_liked_a == 1 then
-- It's a match!
redis.call('HSET', KEYS[1], ARGV[2], ARGV[3])
return 1 -- MUTUAL_MATCH
else
-- Not yet a match; record single like
redis.call('HSET', KEYS[1], ARGV[2], ARGV[3])
return 0 -- SINGLE_LIKE
end
By locking on min(UserA, UserB):max(UserA, UserB), concurrent swipes are serialized, guaranteeing that exactly one worker detects the mutual match and triggers the notification.
Deep Dive 2: Geospatial Indexing (Geohash vs Google S2 vs QuadTree)
How do we find potential candidates within a 25-mile radius without performing slow geospatial joins across 50 Million users?
| Indexing Scheme | Representation | Precision & Boundary Handling | Selected Choice |
|---|---|---|---|
| Geohash (Base32) | Rectangular hierarchical grid string (e.g. 9q8yy) | Boundary problem: Points 1 meter apart can have completely different geohash prefixes across cell boundaries. Requires querying 8 neighboring cells. | Good for simple point caching. |
| QuadTree | Hierarchical 2D space partition tree in memory | Dynamic sub-division based on population density. | Excellent in-memory search, but complex cross-server synchronization. |
| Google S2 (Recommended) | Projects Earth onto 6 cube faces using Hilbert Space-Filling Curve (64-bit integers) | Minimal distortion at poles; spatial locality is preserved linearly. Sub-millisecond radius search. | Google S2 / Uber H3: Fast 64-bit integer comparisons fit perfectly into Redis Sorted Sets. |
Deep Dive 3: Efficiently Excluding Previously Swiped Profiles
If an active user has swiped on 50,000 profiles, how do we exclude those 50,000 users when generating a new recommendation deck?
- Naive Approach:
SELECT * FROM users WHERE user_id NOT IN (SELECT target_id FROM swipes WHERE swiper_id = UserA).- Fatal Flaw: Massive SQL query latency, full index scans, completely unscalable.
- Production Solution:
- User Swiped Set (Redis Set): Store swiped IDs in Redis:
swiped:{user_id}. - Bloom Filter per User: For long-time users with 10,000+ swipes, maintain a Counting Bloom Filter in memory. If
bloomFilter.contains(candidate_id) == true, discard immediately. False positives mean a user is skipped occasionally, which is completely acceptable in a dating app.
- User Swiped Set (Redis Set): Store swiped IDs in Redis:
5. Architectural Trade-Off Matrix
| Design Area | Option A | Option B | Selected Choice & Rationale |
|---|---|---|---|
| Swipe Ingestion | Synchronous Relational DB Write | In-Memory Redis + Async Kafka | Redis + Kafka: Acknowledges swipes in < 20ms. Eliminates relational write bottlenecks during peak evening swiping hours. |
| Match Notifications | Polling every 10s | Persistent WebSocket Connections | WebSockets: Delivers instant dopamine rush of "It's a Match!" while both users are actively engaged in the app. |
| Recommendation Strategy | Pre-compute all decks offline | Hybrid: Pre-fetch spatial candidates + Real-time filter | Hybrid: Pure pre-computation wastes compute on users who change locations; real-time filtering ensures recommendations adapt to current GPS position. |
6. What is Expected at Each Level?
Mid-Level (L4 / IC4)
- Designs data model for Users, Swipes, and Matches.
- Understands the basic two-way like logic for match creation.
- Implements basic geospatial filtering using bounding boxes or spatial databases.
- Proposes push notifications for match alerts.
Senior (L5 / IC5)
- Solves the mutual match race condition using Redis Lua scripts or atomic key locking.
- Explains geospatial indexing trade-offs (Geohash vs Google S2 cells vs QuadTree).
- Optimizes recommendation deck generation by filtering previously swiped profiles via Bloom filters or Redis Sets.
- Handles peak evening traffic spikes (60K+ QPS) using message queues for asynchronous durability.
Staff+ (L6 / Principal)
- Designs global multi-region location roaming: When a user flies from New York to London, how does the system migrate their spatial index without losing pending likes?
- Analyzes shadow-banning, bot detection, and spam prevention architectures (swiping cadence analysis, facial recognition hash verification).
- Details cold-start mitigation for new users to give them immediate profile exposure (algorithmic boost) while maintaining fairness for existing users.
