Design Tinder

MediumSocial & FeedsGeospatialRecommendationsMatchingReal-Time Messaging

Problem

Design a dating app where users set up a profile with photos and preferences (age range, distance, gender) and are shown a deck of nearby profiles one at a time. They swipe right to like or left to pass; when two users like each other, it is a match and both are notified. Matched users can chat with each other. Users must never see the same profile twice in a short period or anyone they already swiped on. The app has 75 million monthly and 20 million daily active users, who make about 2 billion swipes per day.

  • How large is the distance filter: up to 100 km or more, and does it change while travelling?
  • Should recommendations be ranked (by attractiveness, activity, compatibility) or just filtered and random?
  • How quickly should a match be detected and notified: instantly, or within seconds?
  • Can a user be shown again after a pass, and after how long?
  • Are paid features in scope, such as seeing who liked you or unlimited likes?
  • Is chat one-to-one only, and do we need media in messages?

Items in bold are existing requirements. The rest are suggestions. Confirm with the interviewer whether they are relevant first.

  • Create a profile with photos, bio, and discovery preferences.
  • Show a deck of nearby profiles that match each other's preferences.
  • Record left and right swipes.
  • Detect mutual likes and notify both users of a match.
  • Let matched users chat one to one.
  • Never show a profile the user has already swiped on.
  • Update the user's location when the app opens.

Items in bold are existing requirements. The rest are suggestions. Confirm with the interviewer whether they are relevant first.

  • 2 billion swipes per day is about 23,000 per second average and 70,000 at evening peak.
  • Deck requests: each user fetches batches of about 50 profiles, roughly 400 million profile recommendations served per day.
  • Swipe latency under 100 ms; the card animation should never wait on the server.
  • Match notification within 2 seconds of the second like.
  • Swipe history: 2 billion per day at about 30 bytes is 60 GB per day; keep about a year (around 20 TB).
  • Location privacy: never reveal a user's exact location to others, only an approximate distance.

Profiles and preferences live in a profile store, and each user's approximate location is indexed in a geospatial search index sharded by region. A recommendation service queries that index for candidates within distance and preference filters, removes already swiped profiles using a per-user filter, ranks the rest, and caches a deck for the client to consume. Swipes are written to a high-throughput store; a right swipe checks whether the other user already liked back, and if so creates a match and publishes notifications, after which chat runs over a WebSocket gateway.

Loading diagram...

Update location. When the app opens, it sends its coordinates. The profile service rounds them to a coarse grid (about 1 km), stores them, and updates the user's document in the geo search index, which is keyed by geohash and holds age, gender, preferences, and last active time.

Get the deck. The app requests a batch of profiles. The recommendation service queries the geo index for users within the requested distance whose age and gender match the requester's preferences and whose own preferences include the requester. It removes anyone in the requester's seen filter, ranks the remaining candidates, stores the top 200 IDs in a deck cache, and returns the first 50 profiles with photo URLs and approximate distances.

Swipe. The app animates the card immediately and sends POST /swipes in the background (batched if the user swipes quickly). The swipe service writes (swiper, swipee, direction) and adds the swipee to the swiper's seen filter.

Match. On a right swipe, the match service checks whether (swipee, swiper, like) exists. If it does, it creates a match record with a deterministic ID from the sorted user pair (so concurrent likes create it only once) and publishes a match event. Both users get a push notification and, if online, an in-app event over the chat gateway.

Chat. Matched users send messages over a WebSocket to the chat gateway, which verifies the match, stores the message, and delivers it to the recipient's connection or as a push notification.

Stores profiles and makes them searchable by location and filters.

  • PostgreSQL or DynamoDB for profiles; Elasticsearch with geo_point fields, sharded by region; S3 and a CDN for photos.

API

  • PUT /profile with { bio, photos, birthdate, gender, preferences }.
  • POST /location with { lat, lng }.

Data Model

  • Search document: { userId, location (coarse), gender, age, seeking, ageMin, ageMax, maxDistanceKm, lastActiveAt, score }.

Scalability

Indices are sharded by geographic region so a query touches only the shards around the user. Only users active in the last 30 days are indexed, which keeps the index near the 75 million monthly actives. Location updates are throttled to one every few minutes per user.

Tradeoffs

A search engine supports combined geo distance and attribute filters in one query. A pure geohash lookup in Redis is faster for location alone but would require filtering preferences in application code across many more candidates. Rounding coordinates before storage prevents trilateration attacks that reveal a user's exact position.

Builds the deck of candidate profiles for each user.

  • Stateless service in Go or Java; Redis for deck caches.

Diagram

Loading diagram...

Scalability

Each deck build is one search query plus an in-memory filter, and its result serves several batches. Decks are prebuilt in the background for users who are likely to open the app soon (for example, around their usual evening session).

Tradeoffs

Ranking boosts recently active users, because showing inactive profiles produces likes that never become matches. It also boosts users who already liked the requester, which raises match rates but must be mixed with other candidates so a right swipe does not trivially reveal who liked you (a paid feature).

Records swipes at high throughput and tracks who each user has already seen.

  • Cassandra or ScyllaDB for swipes; a per-user Bloom filter in Redis for seen profiles.

API

  • POST /swipes with [{ targetId, direction, ts }] returns any matches created.

Data Model

  • swipes(swiper_id, swipee_id, direction, created_at) partitioned by swiper_id.
  • likes_received(swipee_id, swiper_id, created_at) partitioned by swipee_id, written only for right swipes, used for match checks and "likes you" lists.

Scalability

At 70,000 swipes per second, a write-optimized store with partitioning by user spreads load evenly. A Bloom filter of the profiles a user has swiped (a few thousand to tens of thousands) takes tens of kilobytes and answers membership in microseconds.

Tradeoffs

A Bloom filter has false positives, so a small fraction of unseen profiles are skipped, which users never notice; it has no false negatives, so nobody is shown twice. Querying the swipe table for every candidate would add thousands of reads per deck. Filters are rebuilt from the swipe store if Redis loses them.

Detects mutual likes and creates matches exactly once.

  • Stateless service; DynamoDB or PostgreSQL for matches with a conditional insert.

Data Model

  • matches(match_id PK, user_a, user_b, created_at, unmatched_at) where match_id = hash(min(a,b), max(a,b)).
  • user_matches(user_id, match_id, last_message_at) for the match list.

Tradeoffs

When two users like each other at the same moment, both requests detect the mutual like. The deterministic match ID plus an insert-if-not-exists makes the second insert a no-op, so only one match and one pair of notifications result, without distributed locks.

Delivers real-time messages between matched users.

  • WebSocket gateway nodes with a connection registry in Redis; Cassandra for messages; APNs and FCM for push.

Data Model

  • messages(match_id, message_id, sender_id, body, created_at) partitioned by match_id, clustered by message_id.

Scalability

Peak concurrent connections are a fraction of daily actives, a few million, handled by a modest fleet of gateway nodes. Messages for one conversation live in one partition, so loading history is a single range read.

Tradeoffs

The gateway checks that a match exists and has not been unmatched before accepting a message, which enforces the core rule that only matched users can talk. Unmatching hides the conversation for both users immediately while retaining messages for safety reports.

Related questions