Machine Coding Problem

Tinder Advanced (Elo/Glicko)

macoAllsocialelo/glicko-scoring
Commonly Asked By:Match GroupTinderBumble

Requirements & System Scope

Functional Scope (In-Scope)

  • Dynamic Elo Rating Adjustments: Calculates post-swipe rating modifications for both swipers and swipees based on expected outcomes.
  • Glicko-1 Rating Deviations: Models rating uncertainty (RD) and integrates Glicko expected scores over dynamic match periods.
  • Time-Based Score Uncertainty Decay: Increases rating deviation (RD) exponentially for inactive profiles, reflecting higher uncertainty.
  • Blended Candidate Recommendation Sorters: Balances Elo compatibility differences with cold-start exploration boosts for high-RD new users.

Explicit Boundaries (Out-of-Scope)

  • Real-time Profile Chat Channels: Focuses strictly on rating calculations and ranking recommendations.
  • Geo-Spatial R-Tree Indices: Relies on pre-filtered candidate lists to focus on algorithmic scoring logic.

Class Diagram & Entity Relationships

Rating modifications, score decays, and candidate recommendations:

Loading...
  • UserScore: Manages rating parameters (Elo, Glicko, and deviation metrics).
  • EloUpdater & GlickoUpdater: Modifies user scores after dynamic outcomes.
  • ScoreDecay: Dynamically adjusts uncertainty (RD) based on activity time gaps.

Design Patterns & SOLID Principles

  • Strategy Pattern: Separates ELO updates and Glicko scoring formulas into independent mathematical strategy layers.
  • Decorator Pattern: Decorates recommendations with decay checks and uncertainty boosts prior to final sorting.
  • Single Responsibility Principle (SRP): Decouples swipe registration, ELO expected math, Glicko interval calculations, rating decay timers, and final recommendation sorting.

Core Execution Workflows

Swipe Rating Updates & Uncertainty Decays

  1. Dynamic Swipe ELO Updates:
    1. Load user profile score metrics. Run decay checks first.
    2. Calculate expected outcome percentages: expected = 1 / (1 + 10^((Rb-Ra)/400)).
    3. Apply K-factor boosts for high-uncertainty profiles, calculate score updates, and reset activity timestamps.
  2. Inactivity RD Decays:
    1. Evaluate inactive days: days = timeSinceLastActive / 1 day.
    2. If the user has been inactive for more than 7 days, increase their Rating Deviation: RD = sqrt(RD^2 + c^2 * days), capping it at a maximum value of 350.

Concurrency & Thread Safety Strategy

Securing user score states during highly concurrent swipes:

  • Thread-Safe Profile Metrics: Uses volatile fields and atomic score updates to prevent data corruption during simultaneous swipe calculations.
  • Isolated Thread Pools: Executes rating calculations in worker threads decoupled from API connections, ensuring recommendations are served with low latency.

Complete Clean Code Blueprint

Production reference implementations demonstrating ELO calculators, Glicko-1 formulas, rating deviations, and candidate ranking sorters in Java and Python:

// ─── JAVA BLUEPRINT ──────────────────────────────────────────────────────────
import java.util.*;
import java.util.concurrent.*;

enum SwipeResult {
    LIKE, PASS
}

class UserScore {
    private final String userId;
    private volatile double eloRating;
    private volatile double glickoRating;
    private volatile double ratingDeviation; // RD (uncertainty)
    private volatile long lastActiveAt;

    public UserScore(String userId, double initialRating, double initialRD) {
        this.userId = userId;
        this.eloRating = initialRating;
        this.glickoRating = initialRating;
        this.ratingDeviation = initialRD;
        this.lastActiveAt = System.currentTimeMillis();
    }

    public String getUserId() { return userId; }
    public double getEloRating() { return eloRating; }
    public void setEloRating(double eloRating) { this.eloRating = eloRating; }
    public double getGlickoRating() { return glickoRating; }
    public void setGlickoRating(double glickoRating) { this.glickoRating = glickoRating; }
    public double getRatingDeviation() { return ratingDeviation; }
    public void setRatingDeviation(double ratingDeviation) { this.ratingDeviation = ratingDeviation; }
    public long getLastActiveAt() { return lastActiveAt; }
    public void setLastActiveAt(long lastActiveAt) { this.lastActiveAt = lastActiveAt; }
}

class SwipeOutcome {
    private final String swiperId;
    private final String swipeeId;
    private final SwipeResult result;
    private final long timestampMs;

    public SwipeOutcome(String swiperId, String swipeeId, SwipeResult result) {
        this.swiperId = swiperId;
        this.swipeeId = swipeeId;
        this.result = result;
        this.timestampMs = System.currentTimeMillis();
    }

    public String getSwiperId() { return swiperId; }
    public String getSwipeeId() { return swipeeId; }
    public SwipeResult getResult() { return result; }
    public long getTimestampMs() { return timestampMs; }
}

class EloUpdater {
    private static final double DEFAULT_K = 32.0;
    private static final double NEW_USER_K = 64.0;

    public static void updateRatings(UserScore swiper, UserScore swipee, SwipeResult result) {
        double ratingA = swiper.getEloRating();
        double ratingB = swipee.getEloRating();

        // 1. Calculate expected probability of Swiper liking Swipee
        double expectedA = 1.0 / (1.0 + Math.pow(10.0, (ratingB - ratingA) / 400.0));
        double expectedB = 1.0 - expectedA;

        // Swiper gets rating bump if they choose to LIKE (1.0) vs PASS (0.0)
        double outcomeA = (result == SwipeResult.LIKE) ? 1.0 : 0.0;
        double outcomeB = 1.0 - outcomeA; // Complementary score

        // High K-factor applied for cold-start new users
        double kSwiper = (swiper.getRatingDeviation() > 150.0) ? NEW_USER_K : DEFAULT_K;
        double kSwipee = (swipee.getRatingDeviation() > 150.0) ? NEW_USER_K : DEFAULT_K;

        swiper.setEloRating(ratingA + kSwiper * (outcomeA - expectedA));
        swipee.setEloRating(ratingB + kSwipee * (outcomeB - expectedB));
        
        swiper.setLastActiveAt(System.currentTimeMillis());
        swipee.setLastActiveAt(System.currentTimeMillis());
    }
}

class GlickoUpdater {
    private static final double Q = Math.log(10.0) / 400.0;
    private static final double PI_SQR = Math.PI * Math.PI;

    private static double g(double rd) {
        return 1.0 / Math.sqrt(1.0 + 3.0 * Q * Q * rd * rd / PI_SQR);
    }

    private static double expectedScore(double r, double rOpponent, double gRD_Opponent) {
        return 1.0 / (1.0 + Math.pow(10.0, -gRD_Opponent * (r - rOpponent) / 400.0));
    }

    // Dynamic Glicko-1 Batch Rating Update
    public static void updateGlicko(UserScore user, List<UserScore> opponents, List<SwipeResult> outcomes) {
        double r = user.getGlickoRating();
        double rd = user.getRatingDeviation();
        int matches = opponents.size();

        if (matches == 0) return;

        double dSqrSum = 0.0;
        double ratingDeltaSum = 0.0;

        for (int i = 0; i < matches; i++) {
            UserScore opp = opponents.get(i);
            double rOpp = opp.getGlickoRating();
            double rdOpp = opp.getRatingDeviation();

            double gRD = g(rdOpp);
            double expected = expectedScore(r, rOpp, gRD);
            double outcomeVal = (outcomes.get(i) == SwipeResult.LIKE) ? 1.0 : 0.0;

            dSqrSum += gRD * gRD * expected * (1.0 - expected);
            ratingDeltaSum += gRD * (outcomeVal - expected);
        }

        double dSqr = 1.0 / (Q * Q * dSqrSum);
        double newRD = Math.sqrt(1.0 / ((1.0 / (rd * rd)) + (1.0 / dSqr)));
        double newR = r + (Q / ((1.0 / (rd * rd)) + (1.0 / dSqr))) * ratingDeltaSum;

        user.setGlickoRating(newR);
        user.setRatingDeviation(newRD);
        user.setLastActiveAt(System.currentTimeMillis());
    }
}

class ScoreDecay {
    private static final double DECAY_CONSTANT_C = 1.5; // Controls uncertainty widening speed
    private static final double MAX_RD = 350.0; // Ceiling boundary

    // Increment RD (uncertainty) based on inactivity days
    public static void decayRatingDeviation(UserScore user) {
        long now = System.currentTimeMillis();
        long inactiveDurationMs = now - user.getLastActiveAt();
        double daysInactive = inactiveDurationMs / (24.0 * 3600.0 * 1000.0);

        if (daysInactive >= 7.0) { // Widen uncertainty after 7 days
            double currentRD = user.getRatingDeviation();
            double newRD = Math.sqrt(currentRD * currentRD + DECAY_CONSTANT_C * DECAY_CONSTANT_C * daysInactive);
            user.setRatingDeviation(Math.min(newRD, MAX_RD));
        }
    }
}

class CandidateSorter {
    // Blends Elo compatibility, proximity weights, and cold-start exploration RD boosts
    public static List<UserScore> rankCandidates(UserScore swiper, List<UserScore> candidates) {
        List<UserScore> ranked = new ArrayList<>(candidates);
        ranked.sort((c1, c2) -> {
            double score1 = calculateRankScore(swiper, c1);
            double score2 = calculateRankScore(swiper, c2);
            return Double.compare(score2, score1); // Descending order
        });
        return ranked;
    }

    private static double calculateRankScore(UserScore swiper, UserScore candidate) {
        double eloDiff = Math.abs(swiper.getEloRating() - candidate.getEloRating());
        
        // Base compatibility decreases as ELO difference increases
        double compatibility = Math.max(0.0, 1000.0 - eloDiff);

        // Cold-Start exploration boost: High RD candidates get ranked higher to gather data
        double explorationBoost = candidate.getRatingDeviation() * 0.5;

        return compatibility + explorationBoost;
    }
}

class TinderRankingService {
    private final ConcurrentHashMap<String, UserScore> userScores = new ConcurrentHashMap<>();

    public void registerUser(String userId) {
        // Cold-start settings: Standard Elo 1500, High RD 350 (Maximum uncertainty)
        userScores.put(userId, new UserScore(userId, 1500.0, 350.0));
    }

    public void recordSwipe(String swiperId, String swipeeId, SwipeResult result) {
        UserScore swiper = userScores.get(swiperId);
        UserScore swipee = userScores.get(swipeeId);
        if (swiper == null || swipee == null) return;

        // Apply decay checks for inactivity
        ScoreDecay.decayRatingDeviation(swiper);
        ScoreDecay.decayRatingDeviation(swipee);

        // Perform ELO updates
        EloUpdater.updateRatings(swiper, swipee, result);
    }

    public List<UserScore> getRecommendations(String swiperId, List<String> candidateIds) {
        UserScore swiper = userScores.get(swiperId);
        if (swiper == null) return Collections.emptyList();

        List<UserScore> candidates = new ArrayList<>();
        for (String id : candidateIds) {
            UserScore score = userScores.get(id);
            if (score != null) {
                ScoreDecay.decayRatingDeviation(score); // Decay check before ranking
                candidates.add(score);
            }
        }

        return CandidateSorter.rankCandidates(swiper, candidates);
    }

    public UserScore getUserScore(String userId) {
        return userScores.get(userId);
    }
}

public class Main {
    public static void main(String[] args) {
        System.out.println("=== JAVA TINDER ADVANCED SIMULATION ===");
        TinderRankingService service = new TinderRankingService();

        service.registerUser("alice");
        service.registerUser("bob");
        service.registerUser("charlie");

        System.out.println("Alice initial Elo: " + service.getUserScore("alice").getEloRating());

        // Alice swipes LIKE on Bob
        service.recordSwipe("alice", "bob", SwipeResult.LIKE);

        System.out.println("Alice Elo after swipe: " + service.getUserScore("alice").getEloRating());
        System.out.println("Bob Elo after swipe: " + service.getUserScore("bob").getEloRating());

        List<UserScore> recs = service.getRecommendations("alice", Arrays.asList("bob", "charlie"));
        System.out.println("Recommendations for Alice:");
        for (UserScore u : recs) {
            System.out.println(" - User: " + u.getUserId() + " | Elo: " + u.getEloRating());
        }

        System.out.println("=== END OF JAVA SIMULATION ===");
    }
}

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