Machine Coding Problem

Foursquare Check-In System

macoAllgeovenue-searchbadges
Commonly Asked By:AirbnbFoursquareGoogle

Requirements & System Scope

Functional Scope (In-Scope)

  • Haversine Geo-Radius Search: Filters registered venues within exact kilometer bounds and returns lists sorted by distance.
  • Check-in & State Tracking: Records historical check-in sessions decorated with messages, incrementing venue popularity markers.
  • Multi-Rule Badge Engine: Auto-unlocks milestone honors (Adventurer, Explorer, and Local Hero) based on check-in quantities and category counts.
  • 60-day Mayor Leaderboards: Compares user check-ins over a sliding sixty-day interval to dynamically crown mayors per venue.

Explicit Boundaries (Out-of-Scope)

  • Real-time Friendship Maps: Excludes location sharing between user contacts.
  • Physical Geo-Indexes (Geohash/Quadtree): Employs exact distance math directly over the candidate list to emphasize LLD and mathematical geometry.

Class Diagram & Entity Relationships

Venue indexing, check-in flows, and badge triggers:

Loading...
  • Venue: Keeps track of venue positions, categories, total tallies, and user comments.
  • BadgeEngine: Evaluates user history against a set of unlock criteria.
  • CheckInService: Links user sessions, updates mayor statuses, and filters geo-coordinates.

Design Patterns & SOLID Principles

  • Specification Pattern: Separates badge unlocking rules (Explorer, Local Hero, Adventurer) into discrete rule evaluators.
  • Observer Pattern: Notifies the badge engine and mayor tracker whenever a new check-in is successfully processed.
  • Single Responsibility Principle (SRP): Decouples check-in lifecycle storage, badge evaluation, geo-distance calculations, and mayor tracking.

Core Execution Workflows

Check-ins, Badge Triggers, and Mayor Tracking

  1. Location Check-In Recording:
    1. Register the user check-in event, updating the check-in counters.
    2. Store the event in the historical indexes for both the user and the venue.
  2. Badge Evaluation and Rewards:
    1. Load the user's check-in history.
    2. Evaluate the unlock conditions (e.g. check for at least 1 check-in, 10 check-ins at a single venue, or check-ins across 5 different categories).
    3. Award any newly unlocked badges.
  3. Sliding-Window Mayor Calculation:
    1. Set the cutoff timestamp to 60 days in the past: cutoff = now - 60 days.
    2. Filter the venue's check-in history, keeping only events newer than the cutoff.
    3. Group by user ID, count the occurrences, and crown the user with the highest count as the mayor.

Concurrency & Thread Safety Strategy

Ensuring coordinate sweeps and concurrent check-ins do not block the system:

  • Concurrent Collections: Uses copy-on-write arrays and concurrent map implementations to safely manage user check-in histories across parallel sessions.
  • Synchronized State Tallies: Synchronizes individual venue check-in updates to prevent race conditions during high-volume, concurrent events.

Complete Clean Code Blueprint

Production reference implementations demonstrating distance math, 60-day window rollups, and rule-based badge awards in Java and Python:

// โ”€โ”€โ”€ JAVA BLUEPRINT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;

class Point {
    private final double lat;
    private final double lng;

    public Point(double lat, double lng) {
        this.lat = lat;
        this.lng = lng;
    }

    public double getLat() { return lat; }
    public double getLng() { return lng; }
}

class Venue {
    private final String venueId;
    private final String name;
    private final String category;
    private final Point location;
    private final List<String> tips = new CopyOnWriteArrayList<>();
    private int checkInCount = 0;

    public Venue(String venueId, String name, String category, Point location) {
        this.venueId = venueId;
        this.name = name;
        this.category = category;
        this.location = location;
    }

    public synchronized void incrementCheckIn() {
        this.checkInCount++;
    }

    public void addTip(String tip) {
        this.tips.add(tip);
    }

    public String getVenueId() { return venueId; }
    public String getName() { return name; }
    public String getCategory() { return category; }
    public Point getLocation() { return location; }
    public List<String> getTips() { return tips; }
    public synchronized int getCheckInCount() { return checkInCount; }
}

class CheckIn {
    private final String checkInId;
    private final String userId;
    private final String venueId;
    private final long timestampMs;
    private final String message;

    public CheckIn(String checkInId, String userId, String venueId, String message) {
        this.checkInId = checkInId;
        this.userId = userId;
        this.venueId = venueId;
        this.timestampMs = System.currentTimeMillis();
        this.message = message;
    }

    public String getCheckInId() { return checkInId; }
    public String getUserId() { return userId; }
    public String getVenueId() { return venueId; }
    public long getTimestampMs() { return timestampMs; }
    public String getMessage() { return message; }
}

class Badge {
    private final String badgeId;
    private final String name;
    private final String description;

    public Badge(String badgeId, String name, String description) {
        this.badgeId = badgeId;
        this.name = name;
        this.description = description;
    }

    public String getBadgeId() { return badgeId; }
    public String getName() { return name; }
    public String getDescription() { return description; }
}

class BadgeEngine {
    public static List<Badge> evaluateBadges(List<CheckIn> userHistory, List<Venue> allVenues) {
        List<Badge> unlocked = new ArrayList<>();
        if (userHistory.isEmpty()) return unlocked;

        Map<String, Venue> venueMap = allVenues.stream().collect(Collectors.toMap(Venue::getVenueId, v -> v));

        // Rule 1: Adventurer Badge (First check-in ever)
        if (userHistory.size() >= 1) {
            unlocked.add(new Badge("adventurer", "Adventurer", "First check-in completed!"));
        }

        // Rule 2: Local Hero Badge (10 check-ins at the exact same venue)
        Map<String, Long> venueCheckInCounts = userHistory.stream()
                .collect(Collectors.groupingBy(CheckIn::getVenueId, Collectors.counting()));
        boolean hasLocalHero = venueCheckInCounts.values().stream().anyMatch(c -> c >= 10);
        if (hasLocalHero) {
            unlocked.add(new Badge("local_hero", "Local Hero", "Checked in 10 times at the same venue!"));
        }

        // Rule 3: Explorer Badge (Checked in across 5 distinct venue categories)
        Set<String> uniqueCategories = userHistory.stream()
                .map(c -> venueMap.get(c.getVenueId()))
                .filter(Objects::nonNull)
                .map(Venue::getCategory)
                .collect(Collectors.toSet());
        if (uniqueCategories.size() >= 5) {
            unlocked.add(new Badge("explorer", "Explorer", "Explored venues in 5 unique categories!"));
        }

        return unlocked;
    }
}

class CheckInService {
    private final ConcurrentHashMap<String, Venue> venues = new ConcurrentHashMap<>();
    private final ConcurrentHashMap<String, List<CheckIn>> userHistory = new ConcurrentHashMap<>();
    private final ConcurrentHashMap<String, List<CheckIn>> venueHistory = new ConcurrentHashMap<>();
    private final ConcurrentHashMap<String, Set<String>> userUnlockedBadges = new ConcurrentHashMap<>();
    private static final long SIXTY_DAYS_MS = 60L * 24 * 3600 * 1000;

    public void addVenue(Venue venue) {
        venues.put(venue.getVenueId(), venue);
    }

    public void checkIn(String userId, String venueId, String message) {
        Venue venue = venues.get(venueId);
        if (venue == null) throw new IllegalArgumentException("Venue does not exist!");

        venue.incrementCheckIn();
        String checkInId = UUID.randomUUID().toString();
        CheckIn checkIn = new CheckIn(checkInId, userId, venueId, message);

        userHistory.computeIfAbsent(userId, k -> new CopyOnWriteArrayList<>()).add(checkIn);
        venueHistory.computeIfAbsent(venueId, k -> new CopyOnWriteArrayList<>()).add(checkIn);

        // Evaluate and award badges
        List<Badge> currentBadges = BadgeEngine.evaluateBadges(userHistory.get(userId), new ArrayList<>(venues.values()));
        Set<String> awardedBadgeIds = userUnlockedBadges.computeIfAbsent(userId, k -> ConcurrentHashMap.newKeySet());
        for (Badge b : currentBadges) {
            if (awardedBadgeIds.add(b.getBadgeId())) {
                System.out.println("BADGE UNLOCKED -> User: " + userId + " | Badge: " + b.getName() + " - " + b.getDescription());
            }
        }
    }

    // Resolves Mayor over 60-day window
    public String resolveMayor(String venueId) {
        List<CheckIn> history = venueHistory.get(venueId);
        if (history == null || history.isEmpty()) return "NO MAYOR";

        long cutoff = System.currentTimeMillis() - SIXTY_DAYS_MS;
        Map<String, Long> userCounts = history.stream()
                .filter(c -> c.getTimestampMs() >= cutoff)
                .collect(Collectors.groupingBy(CheckIn::getUserId, Collectors.counting()));

        return userCounts.entrySet().stream()
                .max(Map.Entry.comparingByValue())
                .map(Map.Entry::getKey)
                .orElse("NO MAYOR");
    }

    // Haversine Distance venue search
    public List<Venue> searchVenues(Point center, double radiusKm) {
        List<Venue> results = new ArrayList<>();
        for (Venue venue : venues.values()) {
            double distance = calculateDistance(center, venue.getLocation());
            if (distance <= radiusKm) {
                results.add(venue);
            }
        }
        // Sort by distance ascending
        results.sort(Comparator.comparingDouble(v -> calculateDistance(center, v.getLocation())));
        return results;
    }

    private double calculateDistance(Point p1, Point p2) {
        double R = 6371.0; // Earth's radius in kilometers
        double latDistance = Math.toRadians(p2.getLat() - p1.getLat());
        double lngDistance = Math.toRadians(p2.getLng() - p1.getLng());
        double a = Math.sin(latDistance / 2) * Math.sin(latDistance / 2)
                + Math.cos(Math.toRadians(p1.getLat())) * Math.cos(Math.toRadians(p2.getLat()))
                * Math.sin(lngDistance / 2) * Math.sin(lngDistance / 2);
        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
        return R * c;
    }

    public List<CheckIn> getUserHistory(String userId) {
        return userHistory.getOrDefault(userId, Collections.emptyList());
    }
}

public class Main {
    public static void main(String[] args) {
        System.out.println("=== JAVA FOURSQUARE CHECK-IN SIMULATION ===");
        CheckInService service = new CheckInService();
        
        Venue v1 = new Venue("venue-1", "Coffee Shop", "Food & Drink", new Point(40.7128, -74.0060));
        Venue v2 = new Venue("venue-2", "Gym", "Fitness", new Point(40.7130, -74.0070));
        service.addVenue(v1);
        service.addVenue(v2);
        
        System.out.println("User check-in at Coffee Shop...");
        service.checkIn("user-123", "venue-1", "Enjoying my latte");
        
        System.out.println("Searching venues within 1km...");
        List<Venue> nearby = service.searchVenues(new Point(40.7129, -74.0065), 1.0);
        for (Venue v : nearby) {
            System.out.println("Found venue: " + v.getName() + " | Category: " + v.getCategory());
        }
        
        System.out.println("Mayor of venue-1: " + service.resolveMayor("venue-1"));
        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...