System design interview

Design a swipe-based dating app

Precomputed decks, atomic mutual matches and seen-profile filters.

At a glance

Level
Intermediate
Topics
Location, Caching, Consistency
Main parts
API gateway · Deck service · Swipe store · Swipe service · Deck cache · Refill jobs · Push service · Geo index · Deck builder
What you'll learn
Practise this problem
Overview
PhoneAPI gatewayDeck serviceSwipe storeSwipeserviceDeck cache100 per userRefill jobsPush servicematch alertsGeo indexDeck builderPhoneAPI gatewayDeck serviceserves, filtersSwipe storepairs, seen setsSwipe servicerecords, matchesDeck cache100 per userRefill jobsPush servicematch alertsGeo indexusers by cellDeck builderfilter and rank

Press Play to follow a request through the design, or step through it.

  • Client and edge
  • Service
  • Data store
  • Cache and queue
  1. The user opens the app, and the phone asks for the next profiles.
  2. The gateway routes the request to the deck service.
  3. The service reads this user's ready-made list of 100 candidate IDs from the cache.
  4. The cache returns the list in a millisecond or two. No search happens now.
  5. The service checks the list against this user's seen filter, in case someone was swiped since it was built.
  6. The filter says which candidates are new. The rest are dropped from the response.
  7. The service builds the profile cards, with distances rounded, and returns them.
  8. 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

Key ideas
  • 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 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?
Why it matters: Swipes are the main write load, and they decide whether one database can hold the swipe history.
How is the set of people to show defined: distance, age range, gender preferences?
Why it matters: Preferences must hold in both directions, so each filter shapes the geo index and the candidate query.
Can a profile appear twice for the same user?
Why it matters: If never, we need a compact record of everyone each user has swiped, checked on every deck.
How quickly must a match be shown to both people?
Why it matters: The person who completes the match sees it at once. The other person needs a push, which means an event path.
How exact should shown locations be?
Why it matters: Exact distances can be used to find a person's real position, so locations should be coarse in storage and in the response.

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 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

  1. Only 18 cards are left in this user's deck, below the low mark of 20. The deck service queues a refill job.
  2. A deck builder takes the job, with the user's location and preferences.
  3. 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.
  4. The index returns a few hundred candidates.
  5. The builder keeps those who fit both sides' preferences, ranks them and writes the best 100 to the cache.

Swipe and match

  1. The user swipes right on Sam.
  2. The gateway sends the swipe to the swipe service.
  3. The service does one atomic update on the record for the pair, setting this user's like flag and reading the other flag.
  4. Sam's flag is already set. This operation is the one that completes the pair, so it creates the match.
  5. The service sends Sam a push message, because Sam is not looking at the app.
  6. The response to this swipe says 'it's a match'.
  7. The phone shows the match screen at once.

Step 4 of 6: Go deep on the hard part

Building the deck ahead of time

InterviewerWhy 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

InterviewerTwo 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

InterviewerHow 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

When to find the next profile
Chose
Build a deck of 100 ahead of time
Over
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.

Finding a match
Chose
One atomic record per pair of users
Over
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.

Remembering seen profiles
Chose
A Bloom filter per user, backed by the swipe log
Over
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.

Nearby search
Chose
A grid-cell index, widened ring by ring
Over
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.

What happens when a user travels to a new city?
When the location moves more than a set distance, drop the old deck and queue a fresh build for the new area. Until it is ready, show a short loading state or a small starter deck. Some apps also let users choose a place in advance, which just changes the location the builder uses.
How do you stop a few popular profiles from taking all the attention?
Give each profile a daily exposure budget and lower its rank as the budget is used. The deck builder reads the remaining budget when it ranks. This spreads views more evenly, at the price of showing the most popular people a little less.
How would you catch bots that swipe right on everyone?
Watch the share of likes and the swipe speed per account. An account with 99% likes and hundreds of swipes a minute is almost certainly automated. Hold its likes back, so they create no visible matches, and send it for review.
How do you let a user undo the last swipe?
Keep the last few swipes in an exact list. To undo a like, clear the flag on the pair record, but only if no match has been created yet. Then remove the person from the override list so they can appear again.

Free ATS check

See your resume the way an ATS reads it, then fix what it misses.

Drop in a PDF for a score out of 100, every fix that cost a point, and the line it belongs to. It runs in your browser, with no signup for the score.