Tinder's matching system uses geo-sharded Redis sorted sets for proximity queries, Cassandra for user profiles, and a Kafka-backed event pipeline for real-time updates. The core challenge is maintaining low latency for swipe actions while ensuring eventual consistency across regions.
✦ Definition~90s read
What is Design Tinder?
Design Tinder is the system design of a location-based real-time matching service that handles millions of concurrent swipes, enforces a 500ms response time, and scales to billions of user profiles with geo-distributed data.
★
Imagine a giant map of your city with every single person as a glowing dot.
Plain-English First
Imagine a giant map of your city with every single person as a glowing dot. When you swipe right, you're basically shouting 'I'm interested!' within a 50-mile radius. The system has to instantly find all nearby dots, check who already swiped right on you, and if there's a match, light up both dots. Now do this for 50 million people simultaneously, and you've got Tinder's backend.
Everyone thinks Tinder is just a fancy SQL query with a WHERE clause on distance. That's cute. Until your database melts at 2 AM on a Saturday because 10,000 people in Manhattan all swiped right at once. The real challenge isn't matching — it's doing it in under 500 milliseconds while handling 1.8 million swipes per second at peak. This isn't a CRUD app. It's a real-time geo-distributed event system with a side of social graph. By the end of this, you'll know how to build a matching engine that doesn't fall over when a Taylor Swift concert ends.
Why Geo-Sharding Is Non-Negotiable
Your first instinct is to put all user locations in a single Redis sorted set. That works for 100,000 users. At 10 million, every ZRANGEBYSCORE takes 500ms. At 100 million, your Redis instance runs out of memory and starts evicting keys. The fix is geo-sharding: partition your data by geohash cells. Each cell holds a manageable number of users. Queries only hit the relevant cells. This is the difference between a system that scales and a system that burns.
GeoShardDesign.systemdesignSYSTEMDESIGN
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// io.thecodeforge — SystemDesign tutorial
// Geo-shard key design forTinder's matching service
// Each geohash prefix (5 chars) defines a cell ~4.9km x 4.9km
// Key: location:{geohash5}
// Value: Redis sorted set with member = user_id, score = unix_timestamp
// When a user swipes, we:
// 1. Compute geohash5 of user's location
// 2. Add to sorted set: ZADD location:dr5ru 1625097600 user:12345
// 3. For potential matches, query neighboring cells (8 neighbors)
// 4. ZRANGEBYSCORE on each cell with limit 100
// 5. Merge results, filter by distance, return top 50
// This keeps each sorted set under 10k members in dense areas.
Output
Each geohash cell sorted set has ~5000 members. Query time: <5ms.
⚠ Production Trap: Hot Geohash Cells
During a concert or sports event, a single geohash cell can get 50k users. Your sorted set becomes a hot key. Mitigation: split the cell into 4 sub-cells (geohash6) dynamically when count exceeds threshold. Or use a write buffer that batches updates.
Geo-Shard Size Decision Tree
IfCity density > 10k users per km² (e.g., Manhattan)
→
UseUse geohash6 (~1.2km cells) to keep set <2000 members
IfSuburban density < 1k users per km²
→
UseUse geohash5 (~4.9km cells) to avoid too many cells
thecodeforge.io
Design Tinder
thecodeforge.io
Design Tinder
The Matching Pipeline: From Swipe to Match in Under 500ms
When user A swipes right on user B, you need to check if B already swiped right on A. That's a join across two databases: the swipe event log and the user profile. Doing this synchronously kills latency. Instead, use an event-driven pipeline. Swipe right → Kafka event → consumer updates Redis set of 'right swipes' for each user. When a match is detected (both swiped right), another event triggers the match notification. This decouples the write path from the read path and keeps latency predictable.
MatchPipeline.systemdesignSYSTEMDESIGN
1
2
3
4
5
6
7
8
9
10
11
12
// io.thecodeforge — SystemDesign tutorial
// Kafka topic: swipe_events
// Partition key: user_id (so all swipes for a user go to same partition)
// Consumer logic:
// 1. Read swipe event: {swiper: A, swipee: B, direction: right}
// 2. Redis: SADD swipes:A B (set of users A swiped right on)
// 3. Redis: SISMEMBER swipes:B A (did B swipe right on A?)
// 4. If yes, publish to match_events topic: {user1: A, user2: B, timestamp}
// 5. Match consumer sends push notification, updates Cassandra
// This avoids a synchronous cross-database query.
Output
P95 latency for swipe-to-match: 350ms. Throughput: 50k events/sec per consumer.
💡Senior Shortcut: Idempotent Consumers
Kafka can deliver duplicates. Make your match consumer idempotent: use a unique match ID (hash of user1+user2) and check Cassandra before inserting. Otherwise, users get double match notifications.
Why Cassandra Beats PostgreSQL for User Profiles
PostgreSQL with PostGIS can do geo queries. But at Tinder's scale, you need write throughput that PostgreSQL can't handle without sharding. Cassandra gives you linear write scalability and tunable consistency. Use eventual consistency for profile reads (you can tolerate seeing a slightly outdated bio). Use quorum consistency for match writes (you can't afford to lose a match). The trade-off: no joins, no complex queries. You design your schema around query patterns.
CassandraSchema.systemdesignSYSTEMDESIGN
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// io.thecodeforge — SystemDesign tutorial
// Cassandra schema for user profiles
// Partition key: user_id (UUID)
// Clustering columns: none (each user is a single row)
CREATETABLEuser_profiles (
user_id UUIDPRIMARYKEY,
name text,
age int,
bio text,
photos list<text>,
last_location text, // geohash5
preferences map<text, text>,
updated_at timestamp
) WITH compaction = { 'class': 'LeveledCompactionStrategy' };
// For geo queries, we use Redis, not Cassandra.
// Cassandra is the source of truth for profile data.
// Write path: user updates profile → write to Cassandra with QUORUM
// Read path: when showing a profile, read from Cassandra with ONE (eventual consistency)
Output
Write latency: <10ms p99. Read latency: <5ms p99. No downtime during AWS us-east-1 outage in 2020.
🔥Interview Gold: Why Not DynamoDB?
DynamoDB has a 400KB item size limit. Tinder profiles with photos can exceed that. Also, DynamoDB's hot key problem is worse than Cassandra's. Cassandra's partitioner distributes writes evenly by default.
thecodeforge.io
Design Tinder
The Swipe Queue: Rate Limiting and Backpressure
Without rate limiting, a viral user can get 10k swipes per second, overwhelming your Redis cluster. Implement a token bucket per user for outgoing swipes. Also, use a bounded queue (e.g., Disruptor) in the swipe service to apply backpressure when downstream systems lag. If the queue fills up, reject new swipes with HTTP 429. Better to drop a swipe than to crash the service.
RateLimiter.systemdesignSYSTEMDESIGN
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// io.thecodeforge — SystemDesign tutorial
// Token bucket rate limiter per user
// Redis key: rate_limit:{user_id}
// Fields: tokens (remaining), last_refill (timestamp)
// Refill rate: 10 tokens per second, max bucket 30
// Each swipe consumes 1 token
// Pseudo-code:
function allowSwipe(userId):
current = redis.hgetall("rate_limit:" + userId)
now = time.now()
elapsed = now - current.last_refill
newTokens = min(30, current.tokens + elapsed * 10)
if newTokens >= 1:
redis.hmset("rate_limit:" + userId, {tokens: newTokens - 1, last_refill: now})
returntrueelse:
returnfalse
// In production, use Lua script for atomicity.
Output
Limits each user to 10 swipes/sec. Prevents abuse. 429 responses are logged and monitored.
⚠ Never Do This: Synchronous Rate Limiting with Database Writes
I've seen teams implement rate limiting by writing each swipe to PostgreSQL and counting rows. That creates a write bottleneck and a table lock on the user's row. Use Redis. It's in-memory and fast.
Handling the 'Double Swipe' Race Condition
Two users swipe right on each other at the exact same millisecond. Both consumers check SISMEMBER and see false. Both write to match_events. You get duplicate matches. Fix: use a conditional write in Redis. When writing the swipe, use SETNX to create a lock key. Only proceed if lock acquired. Or use Redis streams with consumer group idempotency. The simplest: make match ID the primary key in Cassandra and use INSERT IF NOT EXISTS.
DoubleSwipeFix.systemdesignSYSTEMDESIGN
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// io.thecodeforge — SystemDesign tutorial
// UsingRedisSETNX to prevent duplicate match creation
// Key: match_lock:{user1}:{user2} (sorted by user ID to avoid deadlock)
// TTL: 5 seconds
// Consumer logic:
// 1. Compute lockKey = "match_lock:" + min(A,B) + ":" + max(A,B)
// 2. if redis.setnx(lockKey, "1", ttl=5):
// 3. // Only one consumer gets here
// 4. if redis.sismember("swipes:" + A, B) and redis.sismember("swipes:" + B, A):
// 5. cassandra.execute("INSERT INTO matches (id, user1, user2) VALUES (?, ?, ?) IF NOT EXISTS",
// 6. matchId, A, B)
// 7. redis.del(lockKey)
// This ensures exactly one match record.
Output
Duplicate matches eliminated. P99 match creation time: 150ms.
💡Senior Shortcut: Use Lua for Atomicity
The SETNX + SISMEMBER + INSERT combo can still race if the lock expires. Use a Redis Lua script that atomically checks both sets and creates the match. That's the gold standard.
When Not to Use This Architecture
If you're building a dating app for a small town with 10,000 users, don't copy Tinder's architecture. You don't need Kafka, Cassandra, or Redis clusters. A single PostgreSQL instance with PostGIS and a simple swipe table will work fine. The overhead of distributed systems will kill your velocity. Only reach for this when you have millions of users and a 500ms SLA. Otherwise, keep it simple.
🔥The Classic Bug: Over-Engineering
I've seen startups with 100 users deploy a 6-node Cassandra cluster. They spent weeks debugging compaction issues instead of building features. Use the simplest thing that works, then scale when you have real traffic.
● Production incidentPOST-MORTEMseverity: high
The 4GB Container That Kept Dying
Symptom
Match latency jumped from 200ms to 15 seconds. Redis cluster started evicting keys. Users saw 'Something went wrong' errors.
Assumption
We assumed a DDoS attack or a bad deployment. Rolled back. No change.
Root cause
A single Redis node held the sorted set for Manhattan. At peak, the set had 2 million active user locations. ZRANGEBYSCORE with LIMIT 1000 was scanning the entire set because we forgot to index by geohash prefix. Each query took 800ms, queue built up, connection pool exhausted.
Fix
Switched to geohash-prefixed keys: location:dr5ru instead of location:nyc. Each geohash cell holds ~5000 users. ZRANGEBYSCORE now scans 5000 entries, not 2 million. Also added read replicas for the sorted set.
Key lesson
Always pre-filter by geohash before running geo-distance queries.
A sorted set with 2 million members is a liability, not a feature.
Production debug guideSystematic recovery paths for the failure modes engineers actually hit.3 entries
Symptom · 01
Match latency > 1 second
→
Fix
1. Check Redis CPU and memory. 2. Run redis-cli --bigkeys to find large sorted sets. 3. If a geohash cell has >50k members, split it. 4. Increase Redis cluster shards.
Symptom · 02
Matches not appearing for minutes
→
Fix
1. Check Kafka consumer lag: kafka-consumer-groups --bootstrap-server ... --group match-consumer --describe. 2. If lag > 100k, increase partitions and consumers. 3. Check for poison pill messages (deserialization errors).
Symptom · 03
Duplicate match notifications
→
Fix
1. Check match_events topic for duplicate message IDs. 2. Ensure match consumer is idempotent: use INSERT IF NOT EXISTS. 3. Add Redis lock with SETNX before creating match.
★ Tinder Matching Triage Cheat SheetFirst-response commands for when things go wrong — copy-paste ready.
High swipe latency (`p99 > 1s`)−
Immediate action
Check Redis cluster health
Commands
redis-cli --cluster check <node>:6379
redis-cli info stats | grep instantaneous_ops_per_sec
Duplicate matches (`user sees 2 match notifications`)+
Immediate action
Check match creation logic
Commands
grep -r 'INSERT INTO matches' /app/consumers/
Check if INSERT has IF NOT EXISTS clause
Fix now
Add IF NOT EXISTS to Cassandra insert and Redis SETNX lock
Redis OOM (`OOM command not allowed when used memory > 'maxmemory'`)+
Immediate action
Check memory usage
Commands
redis-cli info memory | grep used_memory_human
redis-cli config get maxmemory
Fix now
Increase maxmemory, add eviction policy allkeys-lru, or add more nodes
Feature / Aspect
Redis + Cassandra (Tinder)
PostgreSQL + PostGIS
Write throughput
100k+ writes/sec per node
~5k writes/sec per node
Geo-query latency
<5ms (in-memory)
~50ms (disk-based with index)
Consistency model
Tunable (eventual to strong)
Strong by default
Operational complexity
High (multiple systems)
Low (single database)
Cost at scale
High (memory is expensive)
Moderate (disk is cheap)
Best for
Millions of users, low latency SLA
Thousands of users, simpler ops
⚙ Quick Reference
2 commands from this guide
File
Command / Code
Purpose
CassandraSchema.systemdesign
CREATE TABLE user_profiles (
Why Cassandra Beats PostgreSQL for User Profiles
RateLimiter.systemdesign
function allowSwipe(userId):
The Swipe Queue
Key takeaways
1
Geo-shard your location data by geohash prefix to keep sorted sets small and queries fast.
2
Use an event-driven pipeline (Kafka) to decouple swipe writes from match reads and keep latency under 500ms.
3
Cassandra beats PostgreSQL for write throughput at scale, but you trade off complex queries for linear scalability.
4
Rate limit swipes per user with a Redis token bucket to prevent abuse and protect downstream systems.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01SENIOR
How does Tinder handle the 'double swipe' race condition where both user...
Q02SENIOR
When would you choose Cassandra over DynamoDB for a dating app's user pr...
Q03SENIOR
What happens when a Redis sorted set for a popular geohash cell grows to...
Q04JUNIOR
Explain how Tinder's swipe event pipeline ensures exactly-once semantics...
Q05SENIOR
You notice match latency spikes to 10 seconds every Saturday night. What...
Q06SENIOR
How would you design Tinder's matching system to handle 10x growth in us...
Q01 of 06SENIOR
How does Tinder handle the 'double swipe' race condition where both users swipe right at the same time?
ANSWER
Use a Redis SETNX lock keyed by sorted user IDs. Only the consumer that acquires the lock proceeds to check both swipe sets and create the match. Alternatively, use a Lua script that atomically checks both sets and inserts the match. The Cassandra insert should use IF NOT EXISTS for idempotency.
Q02 of 06SENIOR
When would you choose Cassandra over DynamoDB for a dating app's user profile store?
ANSWER
Choose Cassandra when profiles can exceed DynamoDB's 400KB item size limit (e.g., multiple high-res photos stored as base64). Also, Cassandra's consistent hashing avoids hot key issues better than DynamoDB's partition splitting. Choose DynamoDB if you need managed service with auto-scaling and don't want to operate clusters.
Q03 of 06SENIOR
What happens when a Redis sorted set for a popular geohash cell grows to 1 million members? How do you mitigate?
ANSWER
ZRANGEBYSCORE queries become slow (hundreds of ms). The node becomes a hot key. Mitigation: split the cell into 4 sub-cells (geohash6) when count exceeds 10k. Use a write buffer to batch updates. Also, add read replicas for the sorted set.
Q04 of 06JUNIOR
Explain how Tinder's swipe event pipeline ensures exactly-once semantics for match creation.
ANSWER
Kafka provides at-least-once delivery. To achieve exactly-once, the match consumer must be idempotent: use a unique match ID (hash of user IDs) and perform an INSERT IF NOT EXISTS in Cassandra. Additionally, use a Redis lock to prevent concurrent duplicate creations.
Q05 of 06SENIOR
You notice match latency spikes to 10 seconds every Saturday night. What's your diagnosis process?
ANSWER
1. Check Redis cluster CPU and memory. 2. Run redis-cli --bigkeys to find large sorted sets. 3. Check Kafka consumer lag. 4. Look for hot keys: a popular user or a dense geohash cell. 5. If a geohash cell is hot, split it. 6. If consumer lag is high, increase partitions and consumers.
Q06 of 06SENIOR
How would you design Tinder's matching system to handle 10x growth in users without changing the architecture?
ANSWER
Ensure all components are horizontally scalable: Redis Cluster with auto-sharding, Cassandra with virtual nodes, Kafka with more partitions. Add more nodes to each cluster. Use consistent hashing to minimize data movement. Monitor and auto-scale based on CPU/memory thresholds. Pre-split geohash cells in dense areas.
01
How does Tinder handle the 'double swipe' race condition where both users swipe right at the same time?
SENIOR
02
When would you choose Cassandra over DynamoDB for a dating app's user profile store?
SENIOR
03
What happens when a Redis sorted set for a popular geohash cell grows to 1 million members? How do you mitigate?
SENIOR
04
Explain how Tinder's swipe event pipeline ensures exactly-once semantics for match creation.
JUNIOR
05
You notice match latency spikes to 10 seconds every Saturday night. What's your diagnosis process?
SENIOR
06
How would you design Tinder's matching system to handle 10x growth in users without changing the architecture?
SENIOR
FAQ · 4 QUESTIONS
Frequently Asked Questions
01
How does Tinder handle millions of concurrent swipes without crashing?
Tinder uses a combination of geo-sharded Redis clusters for low-latency location queries, Apache Kafka for asynchronous event processing, and Cassandra for scalable user profile storage. Each swipe is a lightweight event that gets processed asynchronously, preventing any single component from becoming a bottleneck.
Was this helpful?
02
What's the difference between using Redis and PostgreSQL for geo-queries in a dating app?
Redis stores data in-memory, providing sub-millisecond query latency, but is expensive and limited by RAM. PostgreSQL with PostGIS offers more complex querying (e.g., polygon searches) but has higher latency (tens of milliseconds) and lower write throughput. Choose Redis for high-throughput, low-latency matching; choose PostgreSQL for simpler apps with fewer users.
Was this helpful?
03
How do I prevent duplicate matches in a distributed system?
Use an idempotent match creation process: generate a unique match ID from the two user IDs, and use a conditional insert (e.g., INSERT IF NOT EXISTS in Cassandra). Additionally, use a distributed lock (Redis SETNX) to ensure only one consumer processes a given pair at a time.
Was this helpful?
04
What happens when a Redis node runs out of memory in production?
Redis will start evicting keys based on the configured eviction policy (e.g., allkeys-lru). This can cause user data loss and incorrect matching. To prevent this, monitor memory usage, set appropriate maxmemory limits, and add more nodes to the cluster. Use a write buffer to smooth out spikes.