Skip to content
System Design

Learn

Design a Dating App (LLD)

Low level design of a dating app: the profile and preference model, filter chain for candidate eligibility, swipe recording, atomic match creation, and safety features.

4 min readUpdated 2 Sept 2026

#LLD#OOP#Chain of Responsibility#Machine Coding

The class-level companion to the Tinder system design. At LLD level the interesting parts are candidate eligibility, which is a stack of independent rules, and match creation, which must be atomic and idempotent.

The domain model

User             id, profile, preferences, subscription, status
Profile          name, age, bio, photos[], location, verified
Preference       ageRange, maxDistance, genders, dealbreakers
Swipe            swiperId, swipeeId, direction, timestamp
Match            id (canonical), userA, userB, createdAt, conversationId
Conversation     matchId, messages
Block / Report   reporterId, targetId, reason, timestamp

Two modelling notes worth stating:

  • `Preference` is separate from `Profile`. What you are and what you want are

different, change independently, and are queried differently.

  • `Match` has a canonical id built from the ordered pair, which is what makes creation

idempotent — see below.

Eligibility: a filter chain

Whether a candidate can be shown is a stack of independent, order-sensitive rules. A single method with eight conditions is the wrong shape; a chain is the right one.

java
interface CandidateFilter {
    boolean accepts(User viewer, User candidate);
}

class BlockFilter implements CandidateFilter {
    public boolean accepts(User viewer, User c) {
        return !blocks.existsBetween(viewer.id(), c.id());   // either direction
    }
}

class AlreadySwipedFilter implements CandidateFilter {
    public boolean accepts(User viewer, User c) {
        return !swipes.exists(viewer.id(), c.id());
    }
}

class MutualPreferenceFilter implements CandidateFilter {
    public boolean accepts(User viewer, User c) {
        // BOTH directions — Tinder shows you only people who could match you
        return viewer.preference().matches(c) && c.preference().matches(viewer);
    }
}

List<CandidateFilter> chain = List.of(
    new ActiveAccountFilter(),
    new BlockFilter(),              // cheapest and most important first
    new AlreadySwipedFilter(),
    new DistanceFilter(),
    new MutualPreferenceFilter()
);

Ordering is deliberate: cheap, high-rejection filters run first so expensive ones see fewer candidates. Adding a rule is adding a class, and each rule is independently unit-testable — which is the argument to make out loud.

Mutual preference is the rule people forget. Filtering only by the viewer's preferences

shows profiles that could never match back, which wastes swipes and is a worse product.

Recording a swipe and creating a match

java
class SwipeService {

    MatchResult swipe(String swiperId, String swipeeId, Direction dir) {
        // 1. Idempotent: re-swiping the same person is a no-op, not an error.
        if (swipes.exists(swiperId, swipeeId)) {
            return MatchResult.alreadySwiped();
        }
        swipes.save(new Swipe(swiperId, swipeeId, dir, Instant.now()));

        if (dir != Direction.RIGHT) return MatchResult.noMatch();

        // 2. Did they already like us?
        Optional<Swipe> reciprocal = swipes.find(swipeeId, swiperId);
        if (reciprocal.isEmpty() || reciprocal.get().direction() != RIGHT) {
            return MatchResult.noMatch();
        }

        // 3. Canonical id ⇒ simultaneous swipes collide on the SAME row,
        //    so the unique constraint makes exactly one match win.
        MatchId id = MatchId.of(swiperId, swipeeId);   // (min, max) ordered
        try {
            Match match = matches.insert(new Match(id, swiperId, swipeeId));
            conversations.create(match);
            notifier.notifyBoth(match);
            return MatchResult.matched(match);
        } catch (DuplicateKeyException e) {
            return MatchResult.matched(matches.get(id));   // the other side won
        }
    }
}

Everything important is in step 3. Ordering the pair means two simultaneous right-swipes target one row rather than two, and the database's unique constraint resolves the race — the loser reads the winner's match rather than creating a second one. Catching the duplicate-key exception and returning success is what makes the operation idempotent under retries.

java
record MatchId(String lower, String higher) {
    static MatchId of(String a, String b) {
        return a.compareTo(b) < 0 ? new MatchId(a, b) : new MatchId(b, a);
    }
}

Deck generation

java
interface DeckStrategy {
    List<String> buildDeck(User viewer, int size);
}

Strategy again, because the ranking rule is the product and will change: nearest-first, most recently active, model-scored by reciprocal likelihood, or a paid boost. The deck is built asynchronously and cached, as described in the system design article — the LLD point is simply that the interface hides which one is in use.

Safety in the model

These are not extensions; in a dating product they are core requirements, and putting them in the domain model rather than in a service is what a good answer looks like.

  • Block is symmetric and permanent — neither user appears to the other again, including

in an existing conversation. It is the first filter in the chain for that reason.

  • Report creates a moderation record without necessarily blocking; repeat reports

escalate.

  • Unmatch deletes the match and the conversation, and must prevent re-matching.
  • Photo verification as a profile flag, and a filter option.
  • Location is stored precisely but exposed as a bucket — exact distances shown to

multiple users allow trilateration of someone's home address, which is a genuine and documented attack.

Patterns worth naming

  • Chain of responsibility — the eligibility filters. Lead with this.
  • Strategy — deck ranking, notification channels.
  • Builder — constructing a profile with many optional fields.
  • Observer — match creation triggering notifications and conversation setup.
  • Repository — swipe and match persistence.

Edge cases to handle

  • Both users swipe right in the same millisecond → the canonical key handles it.
  • A user deletes their account with active matches → tombstone, do not cascade-delete the

other person's conversation history.

  • Undo (rewind) a swipe → the swipe record must be removable, and a resulting match voided.
  • A user changes location or preferences mid-session → the cached deck is stale and must be

rebuilt.

  • Someone blocked after matching → the conversation must become inaccessible immediately.

Extensions to be ready for

  • Super likes and boosts — priority placement in others' decks.
  • Read receipts and typing indicators in the chat.
  • Expiring matches — a conversation that must start within 24 hours.
  • Group and event features, which change the match model from pairwise to n-ary.

Track this problem on the System Design sheet.

Frequently asked

How do you decide which profiles to show a user?

With a chain of independent filters — active account, not blocked in either direction, not already swiped, within distance, and mutual preference — ordered so cheap high-rejection filters run first. Mutual preference is the one candidates forget: filtering only by the viewer's own criteria shows profiles that could never match back.

How do you make match creation safe under simultaneous swipes?

Derive a canonical match id from the ordered user pair, so both racing requests target the same row, and let a unique constraint resolve it. The request that loses the insert catches the duplicate-key error and returns the existing match. That makes creation both race-free and idempotent under client retries.

Why should a dating app show distance buckets rather than exact distances?

Because an exact distance seen from several vantage points allows trilateration of someone's location, which has been demonstrated against real apps. Store precise coordinates for matching but expose only coarse buckets, and never return raw coordinates to clients.

Related