Machine Coding Problem

Tinder Swipe Engine

maco60macoAllsocialmatching-logicgeo-filter
Commonly Asked By:Match GroupTinderBumble

Requirements & System Scope

Functional Scope (In-Scope)

  • Geo-Based Candidate Search: Filter profiles by distance using standard Haversine formulas.
  • Real-time Mutual Match Checking: Detect matching likes instantly and notify both users dynamically.
  • Elo Score Matchmakers: Balance recommendation pools by ranking candidates with similar Elo ratings.
  • Daily Swipe Limits: Limit free-tier accounts to a maximum of 100 swipes per day.

Explicit Boundaries (Out-of-Scope)

  • No Real-time WebSocket Messaging Channels: Excludes live chat servers and media transfers (covered in Slack).
  • No Advanced Profile Photo Moderation: Bypasses automatic image scanning for content moderation.

Class Diagram & Entity Relationships

Structural layout showing relationships between profile indexes, swipe logs, and match services:

Loading...
  • Recommendation Ranks: UserProfile stores geographic coordinates, active preferences, and Elo score ratings.
  • Swipe Ledger Records: SwipeRecord captures swipers, swipees, swipe directions, and timestamps.

Design Patterns & SOLID Principles

  • Observer Pattern (Match Notifications): Decoupling match events from notification dispatchers allows you to add features (push alerts, email summaries) without altering match logic, satisfying Open-Closed Principle.
  • Single Responsibility Principle (SRP):UserProfile stores profiles; SwipeLimiter tracks caps; MatchService handles match detection.

Core Execution Workflows

Swipe & Match Verification Sequence

  1. User swipes right on a profile: swipe(swiperId, swipeeId, LIKE).
  2. Verify the daily swipe limit has not been exceeded.
  3. Write a swipe record:
    1. If PASS, save the record and return.
    2. If LIKE, register the interest in the likes database.
  4. Check for a mutual match:
    1. Query if the swipee has already liked the swiper.
    2. If a mutual like exists, instantiate a Match object.
    3. Publish a match notification event to trigger user alerts.

Concurrency & Thread Safety Strategy

When millions of active users swipe concurrently across the platform:

  • Idempotent Key Matching: Use unique keys sorted by user ID (e.g., userIdA:userIdB) to prevent duplicate match generation when users swipe at the exact same moment.
  • Atomic Swipe Limiters: Enforce swipe caps using thread-safe locks and atomic variables to avoid race conditions under heavy concurrent swiping.

Complete Clean Code Blueprint

Clean reference designs demonstrating matchmaking engines in Java and Python:

// ─── JAVA BLUEPRINT ──────────────────────────────────────────────────────────
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.*;
import java.util.stream.Collectors;

enum SwipeDirection { LIKE, PASS }

// Represents a user profile with coordinates and ELO rating
class UserProfile {
    private final String id;
    private final String name;
    private final String gender;
    private final double latitude;
    private final double longitude;
    private final int age;
    private int eloScore;

    public UserProfile(String id, String name, String gender, int age, double lat, double lon, int eloScore) {
        this.id = id;
        this.name = name;
        this.gender = gender;
        this.age = age;
        this.latitude = lat;
        this.longitude = lon;
        this.eloScore = eloScore;
    }

    public String getId() { return id; }
    public String getName() { return name; }
    public String getGender() { return gender; }
    public int getAge() { return age; }
    public double getLatitude() { return latitude; }
    public double getLongitude() { return longitude; }
    public int getEloScore() { return eloScore; }
    public void updateElo(int opponentElo, SwipeDirection direction) {
        double expected = 1.0 / (1.0 + Math.pow(10, (opponentElo - this.eloScore) / 400.0));
        double actual = (direction == SwipeDirection.LIKE) ? 1.0 : 0.0;
        this.eloScore += (int) (32 * (actual - expected));
    }
}

// Representing a match between User A and User B
class Match {
    private final String id;
    private final String userA;
    private final String userB;
    private final long timestamp;

    public Match(String userA, String userB) {
        // Sort IDs to ensure uniqueness regardless of swiper order
        if (userA.compareTo(userB) < 0) {
            this.userA = userA;
            this.userB = userB;
        } else {
            this.userA = userB;
            this.userB = userA;
        }
        this.id = this.userA + ":" + this.userB;
        this.timestamp = System.currentTimeMillis();
    }

    public String getId() { return id; }
    public String getUserA() { return userA; }
    public String getUserB() { return userB; }
    public long getTimestamp() { return timestamp; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Match)) return false;
        Match match = (Match) o;
        return id.equals(match.id);
    }

    @Override
    public int hashCode() {
        return id.hashCode();
    }
}

// Observer Pattern: Match listener to dispatch notifications
interface MatchListener {
    void onMatchCreated(Match match, UserProfile profileA, UserProfile profileB);
}

// Haversine formula calculation utility
class GeoUtils {
    private static final double EARTH_RADIUS_KM = 6371.0;

    public static double distance(double lat1, double lon1, double lat2, double lon2) {
        double dLat = Math.toRadians(lat2 - lat1);
        double dLon = Math.toRadians(lon2 - lon1);
        double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
                   Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) *
                   Math.sin(dLon / 2) * Math.sin(dLon / 2);
        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
        return EARTH_RADIUS_KM * c;
    }
}

// Engine managing all profile lookups, swipes, and ELO matchmaking
class SwipeEngine {
    private final Map<String, UserProfile> profiles = new ConcurrentHashMap<>();
    
    // Maps: User -> Set of User IDs they have swiped on (avoid showing swiped profiles again)
    private final Map<String, Set<String>> swipedProfiles = new ConcurrentHashMap<>();
    
    // Maps: User -> Set of User IDs who swiped LIKE on them
    private final Map<String, Set<String>> likesReceived = new ConcurrentHashMap<>();
    
    // Daily swipe limits mapping (User -> DateString -> Count)
    private final Map<String, Map<String, AtomicInteger>> dailySwipeCaps = new ConcurrentHashMap<>();
    
    // Active unique matches
    private final Map<String, Match> activeMatches = new ConcurrentHashMap<>();
    
    // Match event listeners
    private final List<MatchListener> listeners = new CopyOnWriteArrayList<>();
    
    private final ReentrantLock matchLock = new ReentrantLock();
    private static final int DAILY_FREE_SWIPE_LIMIT = 100;

    public void registerProfile(UserProfile profile) {
        profiles.put(profile.getId(), profile);
    }

    public void registerMatchListener(MatchListener listener) {
        listeners.add(listener);
    }

    // Recommendation Engine: Return profiles sorted by proximity of ELO scores and Geo distances
    public List<UserProfile> getCandidates(String userId, double maxDistanceKm) {
        UserProfile user = profiles.get(userId);
        if (user == null) return Collections.emptyList();

        Set<String> alreadySwiped = swipedProfiles.computeIfAbsent(userId, k -> ConcurrentHashMap.newKeySet());

        return profiles.values().stream()
            .filter(p -> !p.getId().equals(userId))
            .filter(p -> !alreadySwiped.contains(p.getId()))
            .filter(p -> GeoUtils.distance(user.getLatitude(), user.getLongitude(), p.getLatitude(), p.getLongitude()) <= maxDistanceKm)
            .sorted(Comparator.comparingInt((UserProfile p) -> Math.abs(p.getEloScore() - user.getEloScore()))
                              .thenComparingDouble(p -> GeoUtils.distance(user.getLatitude(), user.getLongitude(), p.getLatitude(), p.getLongitude())))
            .collect(Collectors.toList());
    }

    // Main Swipe Workflow
    public boolean recordSwipe(String swiperId, String swipeeId, SwipeDirection direction) {
        UserProfile swiper = profiles.get(swiperId);
        UserProfile swipee = profiles.get(swipeeId);
        if (swiper == null || swipee == null) {
            System.out.println("Invalid profiles: " + swiperId + " -> " + swipeeId);
            return false;
        }

        // Daily limit check
        String today = new java.text.SimpleDateFormat("yyyy-MM-dd").format(new Date());
        Map<String, AtomicInteger> userCaps = dailySwipeCaps.computeIfAbsent(swiperId, k -> new ConcurrentHashMap<>());
        AtomicInteger counter = userCaps.computeIfAbsent(today, k -> new AtomicInteger(0));
        
        if (counter.get() >= DAILY_FREE_SWIPE_LIMIT) {
            System.out.println("[REJECTED] Daily swipe limit reached for user: " + swiperId);
            return false;
        }

        // Track swipe
        counter.incrementAndGet();
        swipedProfiles.computeIfAbsent(swiperId, k -> ConcurrentHashMap.newKeySet()).add(swipeeId);

        if (direction == SwipeDirection.LIKE) {
            // Register like
            likesReceived.computeIfAbsent(swipeeId, k -> ConcurrentHashMap.newKeySet()).add(swiperId);

            // Double ELO scoring update
            swiper.updateElo(swipee.getEloScore(), SwipeDirection.LIKE);

            // Thread-safe lock to prevent duplicate matches if users swipe at the exact same millisecond
            matchLock.lock();
            try {
                Set<String> swipeeLikes = likesReceived.get(swiperId);
                if (swipeeLikes != null && swipeeLikes.contains(swipeeId)) {
                    Match match = new Match(swiperId, swipeeId);
                    if (!activeMatches.containsKey(match.getId())) {
                        activeMatches.put(match.getId(), match);
                        // Notify listeners
                        for (MatchListener listener : listeners) {
                            listener.onMatchCreated(match, swiper, swipee);
                        }
                    }
                }
            } finally {
                matchLock.unlock();
            }
        } else {
            // Pass
            swiper.updateElo(swipee.getEloScore(), SwipeDirection.PASS);
        }
        return true;
    }

    public Collection<Match> getMatches() {
        return activeMatches.values();
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        SwipeEngine engine = new SwipeEngine();

        // Register default Match Listener
        engine.registerMatchListener((match, profileA, profileB) -> {
            System.out.println("[ALERT] Match Event Triggered!");
            System.out.println("        Mutual Match! " + profileA.getName() + " (ELO: " + profileA.getEloScore() + 
                               ") <==> " + profileB.getName() + " (ELO: " + profileB.getEloScore() + ")");
        });

        // Set up test users (San Francisco bay area location coordinates)
        UserProfile user1 = new UserProfile("u1", "Alice", "Female", 25, 37.7749, -122.4194, 1000); // SF Center
        UserProfile user2 = new UserProfile("u2", "Bob", "Male", 26, 37.7849, -122.4094, 1020);     // 1.4 km from SF
        UserProfile user3 = new UserProfile("u3", "Charlie", "Male", 28, 37.3382, -121.8863, 1500); // San Jose (70km away)
        UserProfile user4 = new UserProfile("u4", "Diana", "Female", 24, 37.7760, -122.4180, 990);  // Close, close ELO

        engine.registerProfile(user1);
        engine.registerProfile(user2);
        engine.registerProfile(user3);
        engine.registerProfile(user4);

        System.out.println("=== 1. GEOGRAPHIC PROXIMITY CANDIDATES FOR ALICE (MAX 5KM) ===");
        List<UserProfile> candidates = engine.getCandidates("u1", 5.0);
        for (UserProfile candidate : candidates) {
            double distance = GeoUtils.distance(user1.getLatitude(), user1.getLongitude(), candidate.getLatitude(), candidate.getLongitude());
            System.out.println("  - " + candidate.getName() + " | Distance: " + String.format("%.2f", distance) + " km | ELO: " + candidate.getEloScore());
        }

        System.out.println("\n=== 2. SWIPE AND REAL-TIME MATCH DETECTION ===");
        // Alice likes Bob
        engine.recordSwipe("u1", "u2", SwipeDirection.LIKE);
        // Bob likes Alice
        engine.recordSwipe("u2", "u1", SwipeDirection.LIKE);

        System.out.println("\n=== 3. CONCURRENT SWIPE ATTACKS ===");
        ExecutorService executor = Executors.newFixedThreadPool(2);
        // Diana and Bob like each other simultaneously
        executor.submit(() -> engine.recordSwipe("u4", "u2", SwipeDirection.LIKE));
        executor.submit(() -> engine.recordSwipe("u2", "u4", SwipeDirection.LIKE));

        executor.shutdown();
        executor.awaitTermination(2, TimeUnit.SECONDS);

        System.out.println("\n=== 4. TESTING SWIPE CAP LIMITS ===");
        UserProfile userLimit = new UserProfile("uLimit", "LimitGuy", "Male", 29, 37.7749, -122.4194, 1000);
        engine.registerProfile(userLimit);
        
        for (int i = 0; i < 105; i++) {
            boolean status = engine.recordSwipe("uLimit", "u1", SwipeDirection.PASS);
            if (!status) {
                System.out.println("Blocked on swipe attempt number: " + (i + 1));
                break;
            }
        }
    }
}

💬Review

Help Us Improve

How helpful was this walkthrough?

Click a star to rate. We actively use this feedback to refine and update our system design content.

Placeholder
Optional but highly appreciated!

Discussion

Share your thoughts, ask questions, or help others.

Loading comments...