Machine Coding Problem

Tinder Swipe Engine

maco60macoAllsocialmatching-logicgeo-filter
Commonly Asked By:Match GroupTinderBumble

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.

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