Section 6: One Swipe, Every Box - High-Level Architecture
Executive Summary: This design is two paths that never wait on each other. The read path is a deck that was ranked minutes ago and sits in Redis. The write path is a swipe that must be durable, checked for a match and answered in under 200 ms. This section draws the boxes, ties each one to a number from Section 3, and walks Priya's Sunday 8:40 p.m. session end to end. It also explains why every asynchronous edge (the refill, CDC into the geo index, notifications, the reconciler) is asynchronous.
The interviewer slides the marker across: "Okay. Draw it." Before the first box goes up, say the sentence the diagram has to respect.
You: "There are two systems here. The deck is a read: precomputed, eventually consistent, and allowed to be a few minutes stale. The swipe is a write: synchronous, durable, and the only place a match can happen. The rule I'll hold to is that nothing the user waits on ever waits on ranking. Let me draw the boxes, then walk Priya through them."
The Boxes
| Component | Job | The number that needs it |
|---|---|---|
| CDN + blob store | Serves every photo; origin never sees card traffic | 400 TB/day of image egress, ~110 Gbps at peak |
| API gateway | Verifies the bearer token, enforces per-account and per-device rate limits on deck fetches and swipe calls, routes | One trust check in front of ~26K swipe requests/s at peak |
| Profile service + DB | System of record for users, preferences, photos (relational, sharded by user_id) | 100M accounts × 2 KB = 200 GB |
| Location service | Snaps GPS to a ~1 km S2 cell and never stores raw coordinates in the index | |
| Photo pipeline | Pre-signed upload, moderation, three renditions; invisible until it passes | 5M uploads/day (~58/s), 250 TB stored |
| Deck service | Pops cards from Redis, re-validates, hydrates profiles | ~3.5K fetches/s at peak, p99 < 300 ms |
| Recs builder | Async refill: retrieve, filter, exclude, rank, budget | ~1.4K refills/s and ~1.4M candidate scorings/s at peak |
| Geo index | Geosharded Elasticsearch, derived and rebuildable | 50M discoverable docs, ~50 GB |
| Swipe + match service | Writes likes and passes, enforces the 100-likes-per-12-hours cap (counting a like only when it creates a new row), runs the match check, creates the match | likes_received), ~21K likes/s, ~700 matches/s at peak |
| Cassandra | Swipes, likes_received, matches, messages, blocks |
~70 TB of swipe data on ~36 nodes |
| Kafka | One event stream for every downstream consumer | Like, match-created, block, deleted, profile-changed |
| Chat + notifications | WebSocket gateways, chat writes, APNs/FCM push | ~3.5K msg/s, 40M match pushes/day |
| Trust & Safety service | Blocks, reports, bans | Block effective everywhere in < 1 s |
To stay readable, the diagram merges profile, location, photo and safety behind the gateway, and folds chat into the push gateways. The table keeps them apart because each one scales and fails on its own schedule.
Interviewer: "Why split swipe and match? Seems like one service."
You: "Deployed, it can be one process. Logically, match creation has two callers: the swipe service's inline check, and the async reconciler that catches matches a timed-out write missed. Both have to go through the same idempotent create keyed on match_id = hash(min(id), max(id)), which writes the match only if no matches row exists for the pair. If that code lives in one place, a duplicate creation collapses into a no-op instead of a second match, and a late replay can't resurrect a pair that has since unmatched."
Sign in to continue reading
The rest of this lesson is available with a free account. Signing in with Google or Microsoft is free.
Sign in to read the full lesson