Mutual Match Detection
A Tinder match happens when two users both swipe right on each other. The moment user B swipes right on user A, the system must detect that A has already swiped right on B and trigger the match notification. This is mutual match detection: an efficient, low-latency check for bidirectional intent.
The Naive Approach#
When user B swipes right on user A: query the swipes table for swiper_id=A AND swiped_id=B AND direction=right. If found: match. This works but has problems at scale. At 18,500 swipes per second, this is 18,500 reads per second against the swipes table, each crossing a shard boundary (B’s swipe is on B’s shard; A’s swipe on A is on A’s shard).
Cross-shard reads add latency and increase database load. You want the match check to be a local operation.
Likes Inbox#
Maintain a separate “likes received” store: when user A swipes right on B, write a record to B’s likes inbox: SET likes:B:A 1. This is a Redis hash or a dedicated table sharded by the person being liked (swiped_id).
When B swipes right on A, the match check is: “does A appear in B’s likes inbox?” — GET likes:A:B. This read is on B’s shard. No cross-shard lookup.
Match Creation#
When a match is detected, create a match record and send push notifications to both users. The match record: (match_id, user_a, user_b, matched_at). This is the entity that unlocks the chat feature between the two users.
Match creation must be idempotent: if B’s swipe is processed twice (at-least-once delivery from the queue), two match records shouldn’t be created. Use INSERT IGNORE or a unique constraint on (min(user_a, user_b), max(user_a, user_b)).
Bloom Filters for Scale#
The likes inbox approach stores one record per right swipe. At 1.6 billion swipes per day, with roughly 50% being right swipes, that’s 800 million new likes inbox entries per day. Storage grows fast.
Bloom filters trade exactness for space. A Bloom filter per user stores the set of people who have liked them, with a small false-positive rate (1%). False positive: show someone a “match” when there isn’t one. This is caught immediately (the real match table doesn’t have the record) and is rarer than 1% in practice. The space savings: 10 bits per entry vs 64+ bytes for a database row.
At Oracle#
We built a bidirectional acknowledgment system for config change propagation: a config change was “confirmed” only when both the primary service and all dependent services acknowledged receipt. Detection was: did all required acknowledgments arrive? We used a Redis set per change, adding service IDs as they acknowledged. When set size reached the expected count, the change was confirmed. Same pattern: local lookup, no cross-shard joins.
What I’m Learning#
Mutual match detection is a specific case of the bidirectional relationship detection problem. The key insight is maintaining a denormalized “likes received” structure that makes the match check a local lookup. Denormalization in the write path buys simplicity and speed in the read path.
Have you built systems that needed to detect bidirectional events efficiently, and how did you handle the consistency between the two sides?