System Design Problem

Design Foursquare (Check-ins and Recommendations)

Commonly Asked By:AirbnbUberFoursquareGoogle

Interview Setup

Interview Prompt

Design a location-based check-in system like Foursquare. Users check in at venues, discover nearby places, get recommendations, earn mayorships, and see friends' activity.

Clarifying Questions (ask before designing)

QuestionWhy it matters
How strict is location verification?Balancing GPS spoofing prevention with user experience: a 200m radius represents the industry standard, because tighter thresholds cause false rejections indoors.
Real-time or async for mayorship and feed updates?
  • Sync path must be < 200ms
  • gamification can lag seconds via Kafka consumers.
How do recommendations balance exploration vs exploitation?
  • Pure CF creates filter bubbles
  • geo + context signals need ε-greedy exploration.

Scope

In scope

  • Check-in with location validation
  • Nearby venue search (< 200ms)
  • Personalized recommendations
  • Mayorship gamification
  • Friend activity feed
  • Tips and venue pages

Out of scope (state explicitly)

  • Turn-by-turn routing and navigation (covered in Map Rendering and Navigation)
  • Payment processing at venues
  • Venue owner merchant dashboard

Functional Requirements

Focus on check-in validation and nearby venue discovery. The system requires location-verified check-ins, friend feeds, and personalized venue recommendations, while clarifying whether tips and reviews fall within scope.

In the room: clarify the check-in radius, because a 50m threshold vs 500m substantially alters fraud detection and indoor user experience.

  • Check-in: Users check in at venues with optional text/photo
  • Venue discovery: Search nearby venues by category, distance, rating
  • Recommendations: Personalized venue suggestions
  • Venue pages: Rich profiles with photos, tips, hours, menu, ratings
  • Tips: Users leave short tips/reviews
  • Lists: Curated venue lists
  • Friends: Social layer: see where friends checked in
  • Mayorships: Most frequent check-in at a venue earns "Mayor" status
  • Trending: Show what's trending nearby right now
  • Explore feed: Discovery feed of nearby popular/new venues

Non-Functional Requirements

Check-in under a few hundred ms and nearby search under 500ms. Spatial queries are the core.

  • Low Latency: Nearby venue search in < 200 ms; recommendations in < 500 ms
  • Location Accuracy: Relevant to user's exact location
  • Scalability: 100M+ venues, 50M+ MAU, 10M+ check-ins/day
  • Freshness: New venues appear in minutes; counts update in real-time
  • Personalization: Recommendations improve with history
  • Availability: 99.99%
  • Privacy: Users control check-in visibility

Capacity Estimations

Daily check-in volume and points of interest count drive the spatial index sharding strategy and stream processing capacity.

MetricCalculationValue
Total venuesGiven (assumption documented in value)100M
DAUGiven (15M daily active users)15M
Check-ins / dayGiven (10M check-ins/day)10M
Check-ins / secGiven~115 (peak 500)
Venue search queries / secGiven (10K QPS peak)10K
Recommendation queries / secGiven (5K QPS peak)5K
Tips / dayGiven (2M tips/day)2M
Total venue data100M x 2 KB200 GB

Architecture Diagram

Check-ins are a write-heavy geospatial event with asynchronous fan-out. We validate proximity server-side against geofences established in Geofencing Service, persist records to PostgreSQL, update mayorship counters in Redis, and publish activity to friend feeds via Kafka, while Elasticsearch powers nearby venue discovery and spatial filtering.

In the room: mayorship is tracked via a per-venue Redis sorted set, so mention atomic ZINCRBY operations and periodic decay windows when interviewers ask about leaderboard mechanics.

Loading...

Component Deep Dives

Check-In Validation and Discovery Flow

The system evaluates client location coordinates against venue coordinates, confirms physical proximity to prevent fraud, and executes high-throughput spatial queries across millions of registered venues.

Venue Discovery: Nearby Search

Approach 1: PostGIS spatial query
  Accurate, supports complex filters. DB query per request → may not handle 10K QPS.

Approach 2: Elasticsearch geo query ⭐
  Full-text + geo + filtering in one query. Handles 10K+ QPS.

Recommended: Elasticsearch for user-facing search; PostGIS for admin queries

Check-in Flow and Kafka Event Pipeline

Topic: checkin-events
  Partitions: 32
  Partition key: user_id (keeps a user's check-ins ordered for idempotency)
  Retention: 7 days
  Replication factor: 3, min.insync.replicas: 2

Producer: Check-in Service after PostgreSQL INSERT (sync path returns 201 first)
  Event: { event_id, checkin_id, user_id, venue_id, lat, lng, shout, visibility, timestamp }

Consumer groups:
  1. feed-writer: LPUSH friend_feed:{friend_id} for visible check-ins
  2. mayorship: ZINCRBY mayor:{venue_id} (atomic, tie-break by timestamp)
  3. venue-stats: INCR venue checkin_count in Redis
  4. trending: ZINCRBY trending:{city} (1-hour sliding window)
  5. recommendations: update user preference vector
  6. notifications: push "Alice checked in at Tartine"

Sync path: validate → anti-fraud → Postgres INSERT → 201 (< 200ms)
Async path: gamification, feeds, and trending via idempotent consumers
DLQ: checkin-events-dlq; alert when mayorship consumer lag > 5 min

Recommendation Service: Personalized Venue Discovery

Recommendation signals:
  1. User Profile Vector (cuisine, price, time preferences)
  2. Collaborative Filtering
  3. Context-Aware: time of day, day of week, weather
  4. Venue Quality: rating, check-in count, tip sentiment

ML Architecture:
  Offline: Spark computes user/venue embeddings (128-dim) nightly
  Online: candidates from ES geo query → score = dot_product x recency x distance x context
  Cache in Redis (TTL: 15 min). Latency budget: ~100 ms total.

Mayorship: Race Conditions and Tie-Breaking

"Mayor" = user with most check-ins at a venue in the last 60 days

Redis: mayor:{venue_id} → Sorted Set { user_id: checkin_count_60d }

On check-in (async consumer):
  ZINCRBY mayor:{venue_id} 1 {user_id}
  current_mayor = ZREVRANGE mayor:{venue_id} 0 0
  if user_id == current_mayor → notify "You're the new mayor!"

Race: two users tie at same count → tie-break by earliest check-in timestamp
  (store score as count * 1e10 - timestamp for lexicographic ordering)

Daily cron: recompute from PostgreSQL for venues with disputed mayorships
  (handles Redis eviction or consumer lag > 24h)

Stale mayor window: if no check-in for 60 days, mayorship expires

API Design

Core Check-in and Discovery Endpoints

The service exposes RESTful endpoints for recording presence, searching local venues, retrieving personalized discovery feeds, and following friend activity.

Check In

HTTP
POST /api/v1/checkins
{
  "venue_id": "v-uuid",
  "lat": 37.7749,
  "lng": -122.4194,
  "shout": "Best croissants ever!",
  "photo_url": "https://cdn.example.com/photos/abc.jpg",
  "visibility": "friends"
}
Response: 201 Created
{
  "checkin_id": "ci-uuid",
  "venue": {"name": "Tartine Bakery", "category": "Bakery"},
  "points_earned": 5,
  "is_mayor": false
}

Common Error Responses

400 Bad Request: missing venue_id or invalid coordinates
401 Unauthorized: invalid token
403 Forbidden: user banned or venue check-in disabled
404 Not Found: venue_id does not exist
409 Conflict: duplicate check-in within 4 hours (idempotent retry returns 200)
422 Unprocessable: user > 200m from venue (location fraud)
429 Too Many Requests: > 20 check-ins/hour (anti-spam)

Search Nearby Venues

HTTP
GET /api/v1/venues/search?q=pizza&lat=37.7749&lng=-122.4194&radius=2000&category=restaurant&min_rating=4.0&limit=20

Get Explore Recommendations

HTTP
GET /api/v1/explore?lat=37.7749&lng=-122.4194&limit=20&time=2025-03-14T19:00:00Z

Add Tip

HTTP
POST /api/v1/venues/{venue_id}/tips
{
  "text": "Try the almond croissant, it is life-changing!",
  "photo_url": "https://cdn.example.com/photos/tip.jpg"
}

Get Friends' Recent Check-ins

HTTP
GET /api/v1/feed/friends?limit=20&cursor={last}

Data Model

PostgreSQL + PostGIS

SQL
CREATE TABLE venues (
    venue_id        UUID PRIMARY KEY,
    name            VARCHAR(255) NOT NULL,
    category_id     INT NOT NULL,
    location        GEOMETRY(Point, 4326) NOT NULL,
    lat             DECIMAL(10,7), lng DECIMAL(10,7),
    address         TEXT, city VARCHAR(100), country_code CHAR(2),
    phone           VARCHAR(20), website TEXT,
    price_tier      SMALLINT, rating DECIMAL(2,1),
    checkin_count   INT DEFAULT 0, tip_count INT DEFAULT 0,
    hours           JSONB, verified BOOLEAN DEFAULT FALSE,
    created_at      TIMESTAMP, updated_at TIMESTAMP
);
CREATE INDEX idx_location ON venues USING GIST(location);

CREATE TABLE checkins (
    checkin_id      UUID PRIMARY KEY,
    user_id         UUID NOT NULL, venue_id UUID NOT NULL,
    lat DECIMAL(10,7), lng DECIMAL(10,7),
    shout TEXT, photo_url TEXT,
    visibility ENUM('public','friends','private') DEFAULT 'friends',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_user (user_id, created_at DESC),
    INDEX idx_venue (venue_id, created_at DESC)
);

CREATE TABLE tips (
    tip_id  UUID PRIMARY KEY,
    venue_id UUID NOT NULL, user_id UUID NOT NULL,
    text TEXT NOT NULL, photo_url TEXT,
    upvote_count INT DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Redis: Caches and Real-time

mayor:{venue_id} → Sorted Set { user_id: count }
venue:{venue_id} → Hash { name, category, rating, lat, lng }
recs:{user_id}:{geohash} → List of venue_ids (TTL: 900)
trending:{city} → Sorted Set { venue_id: check-in count in last hour }
user_emb:{user_id} → Binary (128-dim float32 vector)
friend_feed:{user_id} → List of recent friend check-in IDs

Fault Tolerance

ConcernSolution
Check-in location fraudHaversine distance + velocity check + rate limiting
Elasticsearch downFallback to PostGIS for search (degraded but functional)
Recommendation service down
  • Serve cached recs
  • fallback to popularity-based
Mayorship race conditionRedis ZINCRBY is atomic
Venue data inconsistencyES synced from PostgreSQL via CDC (Debezium)

Additional Considerations

Venue Deduplication & Ranking

User-generated venues duplicate quickly ("Starbucks", "Starbucks Coffee" 40m apart). Offline ML dedup merges candidates before they pollute search ranking; mayorship and trending keys must point at canonical venue_id or counts split across duplicates.

Dedup pipeline:
  1. Normalize name (lowercase, punctuation, abbreviations)
  2. Geo-cluster within 50m
  3. Name similarity (Jaccard/edit distance)
  4. Category match
  5. ML model: combine features → P(same_venue)
  6. P > 0.85 → auto-merge; 0.5-0.85 → human review

Privacy-Preserving Location History

1. Store venue_id, not raw GPS (venue location is public)
2. Check-in aging: after 2 years, remove specific timestamps
3. Embeddings are irreversible
4. Friend visibility: server-side filtering
5. GDPR export/delete cascades to all data

Interview Walkthrough

  • 25-minute cut

    Skip arch50 and arch75 depth unless interviewing for a staff-level role.

    • PostgreSQL for durable check-ins, Elasticsearch for geospatial venue search, Kafka for asynchronous fan-out (5 min)
    • Check-in write path: radius validation, persistence, and mayorship ZINCRBY in Redis (6 min)
    • Anti-fraud mechanisms: 200m geofence radius and velocity checks between consecutive check-ins (5 min)
    • Friend feed fan-out: pushing updates to friend activity streams asynchronously via Kafka consumers (5 min)
    • Staff-level topics: venue deduplication, leaderboards, and privacy controls on location history (4 min)
  • Explain check-in flow: verify user is near venue via geospatial radius, write check-in, and update feeds and leaderboards.
  • Cover venue search via geospatial index and text search hybrid using Elasticsearch geo_query.
  • Discuss duplicate check-in prevention with idempotent request keys.
  • Mention friend activity feed as fan-out-on-write for normal users and fan-out-on-read for high-follower accounts.
  • Cover mayorships and badges as asynchronous stream aggregation jobs rather than inline operations on the check-in path.
  • Common pitfall: accepting check-ins without GPS proximity validation, because spoofed locations corrupt venue analytics.

Engineering Trade-offs

Storage and Spatial Index Trade-Offs

Evaluating database storage engines, spatial indexing strategies, and caching patterns balances ACID safety against sub-millisecond response times. Compare with caching strategies in Caching Patterns & Invalidation and partitioning models in Sharding & Partitioning.

PostgreSQL vs DynamoDB vs Cassandra for Check-ins

PostgreSQL ✓: ACID for check-in + tip in one txn, PostGIS geo queries,
  rich analytics JOINs, ~115 writes/sec (trivial at this scale)
DynamoDB: 25-item transaction limit, no JOINs, cost adds up for geo queries
Cassandra: Overkill for 115 writes/sec, no multi-row transactions

At Foursquare scale (~10M check-ins/day): PostgreSQL is correct.
Scale path: read replicas for analytics, ES for search, Redis for hot counts.

Elasticsearch vs PostGIS for Venue Search

PostGIS: accurate spatial queries, complex polygon filters, but ~100 QPS per shard without tuning. Good for admin/backfill.

Elasticsearch geo_query: inverted index + geo hash grid → 10K+ QPS with filters (category, rating, price). Slight staleness (CDC lag ~1s) acceptable for search. Recommendation: ES for user-facing; PostGIS as source of truth synced via Debezium CDC.

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