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