System design interview
Design a swipe-based dating app
Precomputed decks, atomic mutual matches and seen-profile filters.
At a glance
- Intermediate
- Location, Caching, Consistency
- API gateway · Deck service · Swipe store · Swipe service · Deck cache · Refill jobs · Push service · Geo index · Deck builder
Press Play to follow a request through the design, or step through it.
- Client and edge
- Service
- Data store
- Cache and queue
- The user opens the app, and the phone asks for the next profiles.
- The gateway routes the request to the deck service.
- The service reads this user's ready-made list of 100 candidate IDs from the cache.
- The cache returns the list in a millisecond or two. No search happens now.
- The service checks the list against this user's seen filter, in case someone was swiped since it was built.
- The filter says which candidates are new. The rest are dropped from the response.
- The service builds the profile cards, with distances rounded, and returns them.
- The phone keeps the cards and preloads their photos, so the next swipe is instant.
- Phone
- API gateway
- Deck service: serves, filters
- Swipe store: pairs, seen sets
- Swipe service: records, matches
- Deck cache: 100 per user
- Refill jobs
- Push service: match alerts
- Geo index: users by cell
- Deck builder: filter and rank
On this page
In 2 minutes
- The numbers. A deck query costs far more than a swipe, but decks are built 290 times a second while swipes arrive 23,000 times a second.
- When to find the next profile. Build a deck of 100 ahead of time.
- Finding a match. One atomic record per pair of users.
- Remembering seen profiles. A Bloom filter per user, backed by the swipe log.
- Nearby search. A grid-cell index, widened ring by ring.
Read the 2-minute answerHide the 2-minute answer
I would separate the heavy work from the swipe path. A deck is a list of 100 candidate IDs built ahead of time by a deck builder, which reads the user's grid cell and its neighbours from a geo index, applies both sides' preferences, ranks and caps exposure, and writes the result to a cache. The deck service reads it, re-checks each card and returns it, so the next profile is already on the phone. When 20 cards remain, it queues a refill job.
At 20 million active users that is about 290 builds a second, against 23,000 swipes a second. A swipe is one atomic update on a record for the pair, keyed by the two user IDs in order, setting my flag and returning the other. The later of two simultaneous swipes sees both flags and creates the match, once, using a conditional set. The first person gets a push, deduplicated by match ID.
To never repeat a profile, each user has a Bloom filter, about 25 KB, which is 6.4 times smaller than exact IDs, backed by the swipe log. It never shows a profile twice. It hides about 1% of unseen ones by mistake, and I would rebuild it when it fills. Locations are coarse.
Step 1 of 6: Clarify the scope
Questions to ask first
Ask these before you draw anything. Guess why each answer changes the design, then open it to check.
How many users are there, and how many swipes do they make in a day?
How is the set of people to show defined: distance, age range, gender preferences?
Can a profile appear twice for the same user?
How quickly must a match be shown to both people?
How exact should shown locations be?
Requirements
Functional
- Show a stack of profiles that fit the viewer's location and both sides' preferences.
- Record a like or a pass for each swipe.
- Detect a mutual like, create exactly one match and tell both people.
- Never show a profile the user has already swiped.
- List a user's matches and open a chat for each.
Non-functional
- The next profile shows at once because the next 100 are already on the device.
- Fetching a deck: p99 under 200 ms.
- Recording a swipe: p99 under 100 ms.
- Two people who like each other at the same moment get exactly one match, never none and never two.
- The person who liked first gets the match notification within 5 seconds.
Step 2 of 6: Size it
Show the mathsHide the maths
These numbers are assumptions for the exercise, not facts about a real system:
- There are 100 million registered users, and 20 million are active in a day. Each active user swipes 100 times a day, and 30% of swipes are likes. The evening peak is 3 times the average.
- 2% of likes complete a mutual match.
- A deck holds 100 profiles. The app asks for more when 20 remain, so each refill adds 80.
- A swipe record takes 24 bytes. A user has seen 20,000 profiles over their lifetime on average.
The arithmetic:
- Swipes per second
- 20M × 100 = 2B a day ÷ 86,400 s ≈ 23,100
- ≈ 23,000/s on average, ≈ 69,000/s at the peak
- Likes per second
- 30% × 23,148
- ≈ 6,900/s on average, ≈ 21,000/s at the peak
- Matches per day
- 2B × 30% = 600M likes × 2%
- 12 million, about 139/s
- Deck builds per second
- 100 swipes ÷ 80 per refill = 1.25 refills × 20M users = 25M ÷ 86,400 s
- ≈ 290/s, small compared with the swipe rate
- Swipe log growth
- 2B swipes × 24 bytes
- 48 GB a day, about 17.5 TB a year
- Seen sets for all users
- Exact: 20,000 × 8 B = 160 KB × 100M. Bloom filter: 20,000 × 10 bits = 25 KB × 100M
- 16 TB exact, or 2.5 TB as Bloom filters
Step 3 of 6: Sketch the path of one request
Step through the diagram at the top of the page to watch a request move through these parts.
The phone talks to an API gateway. When the app opens, it asks the Deck service for a deck. The service reads a ready-made list of 100 candidates from the Deck cache, drops any the user has already swiped by checking the Swipe store, and returns the profile cards. If fewer than 20 remain, it puts a job on the Refill jobs queue.
A Deck builder takes the job. It asks the Geo index for people in the user's grid cell and the cells around it, keeps those who fit both sides' preferences, ranks them, and writes the best 100 to the deck cache. Nothing in a swipe waits for this.
Each swipe goes to the Swipe service, which records it in the swipe store with one atomic operation on a record for the pair of people. If that operation shows that the other person already liked, it is a match. The service tells the second person in the response, and tells the first through the Push service.
Other paths through the system
Refill a deck
- Only 18 cards are left in this user's deck, below the low mark of 20. The deck service queues a refill job.
- A deck builder takes the job, with the user's location and preferences.
- It asks the geo index for people in the user's cell and the cells around it, widening the ring if too few are found.
- The index returns a few hundred candidates.
- The builder keeps those who fit both sides' preferences, ranks them and writes the best 100 to the cache.
Swipe and match
- The user swipes right on Sam.
- The gateway sends the swipe to the swipe service.
- The service does one atomic update on the record for the pair, setting this user's like flag and reading the other flag.
- Sam's flag is already set. This operation is the one that completes the pair, so it creates the match.
- The service sends Sam a push message, because Sam is not looking at the app.
- The response to this swipe says 'it's a match'.
- The phone shows the match screen at once.
Step 4 of 6: Go deep on the hard part
Building the deck ahead of time
Why not search for the next profile on every swipe, and how do you build a deck instead?A search for 'nearby people who fit both sides' preferences, not yet seen, best first' is heavy.
A search for 'nearby people who fit both sides' preferences, not yet seen, best first' is heavy. At 69,000 swipes a second, running it per swipe is out of the question. So we do it in advance, in bulk, and the swipe path only reads a list.
Finding candidates. Index users by a grid cell of their location, such as a geohash or a hexagonal cell, with cells a few kilometres wide. To build a deck, read the user's own cell and its neighbours. If there are too few people, widen by one ring at a time, up to the user's maximum distance. In a dense city, cells can hold a lot of people, so cap the sample. Collect around 500 candidates.
Filtering. Preferences must hold both ways: I want to see them, and they want to see me. Also drop people who are inactive for weeks, blocked, or reported. Do not check the seen list here; it is applied when the deck is served, so it is always fresh.
Ranking. Keep it simple at first: recent activity, profile completeness, distance, and a strong boost for people who have already liked this user, since showing them leads to a match. Add an exposure cap, so that a few popular profiles are not shown to everyone while the rest are never seen.
Storing and refilling. Write the best 100 IDs, a few hundred bytes, to the cache with a one-day expiry. The deck service refills when 20 remain, by queueing a job. A builder takes about 100 to 300 ms per deck, and at 290 builds a second a modest pool of workers is enough. Rebuild immediately when the user changes preferences or moves more than, say, 10 km.
Since a candidate can change after the deck is built, the service re-checks the few cards it returns: still active, not blocked, not deleted. Never expose exact coordinates. Store a cell, and round the distance up to the next kilometre, because exact distances from several fake positions can be used to pinpoint someone.
Atomic detection of a mutual like
Two people like each other at nearly the same moment. How do you guarantee exactly one match?The tempting design is a table of swipes with the swiper and the target.
The tempting design is a table of swipes with the swiper and the target. When Ana likes Sam, check whether Sam already liked Ana, then write Ana's like. Now suppose both swipes arrive together. Ana's check sees nothing, because Sam's write is not there yet. Sam's check sees nothing for the same reason. Both then write. The pair liked each other and nobody ever finds out.
The fix is to give each pair one record, keyed by the two user IDs in a fixed order, such as the smaller ID first. The record holds two flags: whether the first person liked, and whether the second liked. Each swipe is a single atomic operation on that record:
- Create the record if it is missing, set my flag, and return the other flag in the same step.
- Operations on one record run one after the other. So of the two swipes, one is first and one is second.
- The second one sees that the other flag is already set. It is the one that creates the match, by setting the matched time only if it is still empty.
- A retry of the same swipe finds its flag already set and the match already made, so it does nothing. That makes the call safe to repeat.
This works with a conditional update in a SQL database, a conditional write in a key-value store, or a small script in an in-memory store. Hash the pair key to choose the shard, and both swipes land on the same one. Then single-record atomicity is enough, and no cross-shard transaction is needed. Popular profiles do not create a hot key, because each pair has its own record.
A pass needs no pair record. Add it to the swiper's seen filter and move on. Keep a second, per-user write for the seen filter and the list of who liked me, and update it right after the pair record. If it is lost, rebuild it from the pair records.
Telling people. The person who completes the match gets 'it's a match' in the swipe response. The other person is not looking, so publish a match event. It sends a push message and opens the chat room, and both steps use the match ID as an idempotency key, so a repeated event does not create two rooms or two alerts.
Never showing a profile twice
How do you remember everyone a user has swiped, cheaply, and filter candidates against it?The swipe log is the truth, but it is too slow to scan for each deck.
The swipe log is the truth, but it is too slow to scan for each deck. We need a compact structure per user that answers 'have I swiped this person?' in microseconds. A user may have swiped 20,000 people over time.
Storing the exact IDs takes 20,000 × 8 bytes = 160 KB per user, or 16 TB for 100 million users. It works, but it is a lot of memory to keep close to the deck service.
A Bloom filter is smaller. It is an array of bits plus several hash functions. To add an ID, set the bits chosen by each hash. To test an ID, look at those bits. If any bit is 0, the ID is definitely new. If all are 1, it was probably seen. It never says 'new' for someone who was seen, so it cannot show a profile twice.
For 1% wrong 'probably seen' answers you need about 9.6 bits per entry and 7 hash functions. For 20,000 entries that is about 25 KB per user, so 2.5 TB in all, 6.4 times smaller than exact IDs.
The honest cost is the false positives. About 1 in 100 unseen candidates is hidden by mistake. The user never knows, since they do not see what they are missing, and with hundreds of candidates it loses almost nothing. Two more limits matter:
- Filters fill up. If a power user reaches 40,000 entries in a filter built for 20,000, the false-positive rate climbs above 10%. Watch the count, and rebuild a bigger filter from the swipe log when it passes the limit.
- You cannot remove an entry. For 'undo my last swipe' or 'reset my seen list', keep the last few swipes as a short exact list that overrides the filter, and rebuild the filter for a reset. Keep one filter per 90 days and check the latest four, so profiles that were passed long ago can come back.
The Bloom filter is only a cache. Keep the exact swipe log as the truth and rebuild any filter from it after a loss.
Step 5 of 6: Name the trade-offs
- Build a deck of 100 ahead of time
- Search on every swipe
Swipes stay instant and the heavy query runs only 290 times a second. The cost is that a deck can be slightly stale, so it is re-checked when served.
- One atomic record per pair of users
- Read the other person's like, then write mine
Exactly one of two simultaneous swipes completes the match. The cost is that the store needs atomic conditional updates, and shards by pair.
- A Bloom filter per user, backed by the swipe log
- An exact set of IDs per user
It is 6.4 times smaller and fast to test. The cost is about 1% of unseen people hidden by mistake, and no removals.
- A grid-cell index, widened ring by ring
- Sort all users by exact distance
Reading a few cells is cheap and easy to shard. The cost is cell edges, which we fix by reading neighbouring cells.
Step 6 of 6: Say what breaks next
Follow-up questions an interviewer asks once the design is on the board. Answer each one out loud, then open it to compare.