Machine Coding Problem

Foursquare Check-In System

macoAllgeovenue-searchbadges
Commonly Asked By:AirbnbFoursquareGoogle

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.

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 ===");
    }
}