System Design Problem

Design a Geofencing Service

Commonly Asked By:UberLyftDoorDashGoogle

Interview Setup

Interview Prompt

Design a geofencing service that lets apps define virtual boundaries and get notified when devices enter, exit, or dwell inside them. Think store arrival alerts, fleet zone monitoring, or delivery drop-off detection.

Clarifying Questions (ask before designing)

QuestionWhy it matters
How many active geofences and devices are we supporting?100M fences with 10M concurrent devices drives R-tree sharding and Flink partition strategy.
What's the acceptable trigger latency?Under 5 seconds requires streaming evaluation, not batch cron over location history.
Do we need dwell detection or just enter/exit?Dwell adds Redis TTL timers and hysteresis buffers to filter GPS jitter near boundaries.
How do clients send location: streaming or batched?Batch uploads from mobile with spotty connectivity change Kafka ingest pattern and state reconciliation.

Scope

In scope

  • Geofence CRUD with polygon and circle types
  • Streaming location evaluation at 5M updates/sec
  • Enter, exit, and dwell event detection
  • Webhook and push notification delivery
  • Spatial indexing with R-tree two-phase filter
  • Multi-tenant fence management

Out of scope (state explicitly)

  • Turn-by-turn navigation (covered in Map Rendering and Navigation)
  • Full fleet dispatch and routing
  • Device firmware and GPS hardware

Functional Requirements

Clarify fence shapes, trigger events, and device update rate. The service requires geofence registration, streaming location ingestion from devices, and reliable enter, exit, and dwell event delivery.

In the room: GPS jitter at the boundary causes enter and exit flapping, so emphasize why hysteresis buffers and dead zones are necessary.

  • Create geofences: Define virtual geographic boundaries with metadata
  • Real-time trigger: Detect enter, exit, or dwell events
  • Event notifications: Fire webhooks/push on geofence triggers
  • Bulk management: Support millions of active geofences
  • Geofence types: Static, dynamic, time-based
  • Dwell detection: Trigger after user stays inside for N seconds
  • Geofence groups: Group by category
  • Analytics: Track entry/exit counts, dwell time
  • Multi-tenant: Support multiple clients

Non-Functional Requirements

Enter/exit detection within seconds of GPS update. Millions of devices reporting location periodically.

  • Low Latency: Geofence check in < 10 ms per device update
  • Real-time: Event triggered within 5 seconds
  • Scale: 100M+ active geofences, 10M+ devices
  • Throughput: 5M location updates/sec at peak
  • Accuracy: Handle geofences as small as 50m radius
  • Availability: 99.99%
  • Durability: No trigger events lost (at-least-once delivery)
  • Battery Aware: Minimize GPS polling frequency

Capacity Estimations

Device count multiplied by update frequency and fence volume drives the throughput of the spatial index and stream evaluation workers.

MetricCalculationValue
Active geofencesGiven (assumption documented in value)100M
Active devicesGiven (assumption documented in value)10M concurrently
Location updates / secFrom Location updates / day ÷ 86400 (+ peak factor in value)5M peak
Avg geofence polygon verticesGiven (typical workload assumption)6
Geofence storage per fenceGiven~500 bytes
Total geofence data100M x 500B50 GB
Trigger events / secGiven~50K
Event notification deliveryGiven (assumption documented in value)< 5 seconds

Architecture Diagram

Geofencing matches streaming GPS points against fence polygons, sharing ingest patterns with Real-Time Vehicle Tracking and spatial partitioning with Map Rendering & Navigation. We index fences in an R-tree or geohash grid for coarse filtering, run point-in-polygon algorithms on candidate fences, and track enter or exit state per device-fence pair in Redis with hysteresis buffers to absorb GPS jitter at boundaries.

In the room: synchronous inline evaluation works for dozens of fences; at thousands per device, move to asynchronous stream processing with pre-filtered candidates.

Loading...

Component Deep Dives

Two-Phase Spatial Evaluation Flow

GPS updates flow from ingestion through spatial candidate queries, state transition evaluations (inside vs outside), and downstream event delivery. Hysteresis buffers prevent boundary flutter caused by noisy GPS readings.

The expensive part is not the polygon geometry math, because ray casting across 6 vertices completes in microseconds. The primary scalability challenge is narrowing 100M global geofences down to a handful of local candidates per location update, followed by tracking per-device state so enter and exit events fire exactly once. For the underlying stream processing architecture, see Stream Processing Basics.

Spatial Index: Two-Phase Filtering

Phase 1: Coarse filtering with spatial index (R-tree or Geohash)
  Each geofence has a bounding box → R-tree indexes these
  Query: "Which bounding boxes contain (lat, lng)?"
  Result: ~10-50 candidate geofences (from 100M → 50)
  Time: O(log N)

Phase 2: Precise point-in-polygon test (ray casting)
  For each candidate, ray from point to infinity
  Count intersections with polygon edges
  Odd → inside; Even → outside
  Time: O(V) per polygon (~300 operations → microseconds)

Total: < 1 ms per location update

Geofence Evaluation Engine: Processing 5M Updates/Sec

Architecture: Flink streaming application

Kafka Topic: location-updates (256 partitions by device_id)

Flink Job:
  1. Consume location updates
  2. Query spatial index (R-tree via sidecar)
  3. Get candidate geofences (10-50)
  4. Point-in-polygon check for each
  5. Compare with previous state (Redis lookup)
  6. If state changed → emit trigger event to Kafka

Scaling: Partition by geohash region, each worker loads only its region's fences

Dwell Detection: Preventing False Triggers

On ENTER: Start a dwell timer (e.g., 60 seconds)
  SET dwell_timer:{device}:{fence} 1 EX 60

On each update inside: check if timer expired → trigger DWELL event
On EXIT before expiry: delete timer → pass-through ignored

GPS Jitter handling: Hysteresis buffer
  Enter threshold: > 10m inside boundary
  Exit threshold: > 10m outside boundary
  Dead zone: within 10m → maintain current state

Event Bus Design (Kafka)

Topics:
  location-updates: partition key: device_id (high-volume GPS stream)
  geofence-triggers: partition key: geofence_id (exit and enter events)
  Partitions: 256 / 32; Retention: 48h / 7 days; RF=3

Producer events:
  location-updates: { device_id, lat, lng, accuracy_m, speed, timestamp }
  geofence-triggers: { device_id, geofence_id, event: enter|exit, timestamp }

Consumer groups:
  1. geofence-evaluator: Flink spatial join against R-tree index to emit triggers
  2. trigger-delivery: webhooks, push notifications, and SMS via Trigger Delivery Service
  3. event-store: append to ClickHouse for audit and analytics

Sync path: POST /location → Kafka location-updates → 202 (never block on evaluation)
Point-in-polygon query path: Geofence Query Service uses in-memory spatial index
DLQ: location-updates-dlq; alert when evaluator lag > 30s

API Design

Geofence Lifecycle APIs

The service exposes endpoints to register virtual boundaries, stream batched client locations, retrieve event history, and inspect active devices inside a fence perimeter.

Create Geofence

HTTP
POST /api/v1/geofences
{
  "name": "Downtown Store 42",
  "type": "polygon",
  "coordinates": [
    {"lat": 37.7749, "lng": -122.4194},
    {"lat": 37.7759, "lng": -122.4184},
    {"lat": 37.7739, "lng": -122.4174},
    {"lat": 37.7729, "lng": -122.4184}
  ],
  "triggers": ["enter", "exit", "dwell"],
  "dwell_time_seconds": 120,
  "metadata": {"store_id": "s-42"},
  "webhook_url": "https://client.example.com/geofence-events"
}
Response: 201 Created { "fence_id": "fence-uuid", "status": "active" }

Batch Location Update

HTTP
POST /api/v1/locations/batch
{
  "device_id": "device-uuid",
  "locations": [
    {"lat": 37.7749, "lng": -122.4194, "accuracy": 10, "timestamp": 1710400000},
    {"lat": 37.7751, "lng": -122.4190, "accuracy": 8, "timestamp": 1710400005}
  ]
}

Get Geofence Events

HTTP
GET /api/v1/geofences/{fence_id}/events?start=2025-03-14T00:00:00Z&end=2025-03-14T23:59:59Z

Query Active Devices

HTTP
GET /api/v1/geofences/{fence_id}/devices?status=inside
→ { "devices": [...], "count": 42 }

Common Error Responses

400 Bad Request: invalid input, missing required fields, or malformed JSON payload
401 Unauthorized: missing or invalid authentication token or API key
403 Forbidden: authenticated caller lacks required permissions for this resource
404 Not Found: requested resource ID does not exist
409 Conflict: duplicate write or version conflict, retry with a unique idempotency key
422 Unprocessable Entity: syntactically valid request failed semantic business validation
429 Too Many Requests: rate limit quota exceeded, client should honor Retry-After header
500 Internal Error: unexpected server failure, retry safely with an idempotency key
503 Service Unavailable: downstream dependency is unavailable or overloaded, retry with exponential backoff
440 Login Timeout: WebSocket connection session expired, client reconnect is required

Data Model

PostgreSQL and PostGIS: Geofence Definitions

SQL
CREATE TABLE geofences (
    fence_id        UUID PRIMARY KEY,
    app_id          UUID NOT NULL,
    name            VARCHAR(255),
    fence_type      ENUM('polygon', 'circle') NOT NULL,
    geometry        GEOMETRY(Polygon, 4326),
    center_lat      DECIMAL(10,7),
    center_lng      DECIMAL(10,7),
    radius_meters   INT,
    triggers        JSONB,
    dwell_seconds   INT DEFAULT 0,
    metadata        JSONB,
    group_id        UUID,
    webhook_url     TEXT,
    active          BOOLEAN DEFAULT TRUE,
    active_hours    JSONB,
    created_at      TIMESTAMP,
    updated_at      TIMESTAMP
);
CREATE INDEX idx_geometry ON geofences USING GIST(geometry);

Redis: Device State and Dwell Timers

device_fence:{device_id}:{fence_id} → Hash { state, entered_at }
dwell_timer:{device_id}:{fence_id} → "1" (TTL: dwell_seconds)
device_loc:{device_id} → Hash { lat, lng, accuracy, timestamp }
fence_device_count:{fence_id} → INT

ClickHouse: Event Analytics

SQL
CREATE TABLE geofence_events (
    event_id        UUID,
    fence_id        UUID,
    device_id       UUID,
    app_id          UUID,
    event_type      Enum8('enter'=1, 'exit'=2, 'dwell'=3),
    timestamp       DateTime,
    lat             Float64,
    lng             Float64,
    dwell_seconds   UInt32
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(timestamp)
ORDER BY (app_id, fence_id, timestamp);

Fault Tolerance

ConcernSolution
Evaluation engine failureFlink checkpointing and consumer group rebalance
Spatial index crash
  • Rebuild from PostGIS (2-3 min for 100M fences)
  • replicas serve during rebuild
Redis state lossRedis Cluster with AOF persistence
Webhook delivery failure
  • Exponential backoff retry (max 5)
  • dead letter queue routing
Duplicate triggersIdempotent processing: deduplicate by (device, fence, type, 5-min window)
GPS accuracy poorIgnore updates with accuracy greater than 100 meters

Additional Considerations

Battery Optimization: Adaptive Location Frequency

Far from geofence (>5km): update every 5 min (cell tower) → negligible battery
Approaching (1-5km): update every 60 sec (WiFi-assisted) → ~1%/hr
Near boundary (<1km): update every 10 sec (GPS) → ~3%/hr
Inside fence (dwell): update every 30 sec → ~2%/hr

Client-Side vs Server-Side vs Hybrid

Hybrid Approach ⭐:
  1. Server manages all 100M geofences
  2. For each device, server identifies 20 nearest fences
  3. Push these 20 to client's native geofencing API
  4. Client triggers instantly (offline-capable)
  5. Client also sends location updates for fences beyond the 20

Per-Subscription Fence Subsets

Scanning 100M global fences per GPS update does not scale. Index fences by geohash cell; for each device subscription, maintain only the fence IDs that intersect the user's recent cells (typically 20 to 50). Push that subset to the client for offline enter/exit; server reconciles on reconnect.

Interview Walkthrough

  • 25-minute cut

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

    • Point-in-polygon and radius checks against geofence definitions (5 min)
    • Geohash grid indexing to narrow candidate fences before evaluation (6 min)
    • Event-driven triggers: location ingestion, candidate matching, and queue delivery (5 min)
    • Synchronous inline evaluation vs asynchronous stream processing trade-offs (5 min)
    • Fence versioning when businesses update geographical boundaries (4 min)
  • Frame the core evaluation as a point-in-polygon or radius check against geofence definitions, using a batch spatial pre-filter before executing exact geometry algorithms.
  • Explain geohash grid indexing to narrow candidate fences before running precise intersection tests.
  • Cover event-driven triggers where incoming location updates match candidate fences and publish enter or exit events directly to a streaming queue.
  • Discuss synchronous inline evaluation with low latency for limited fences vs asynchronous evaluation for millions of fences with seconds of delay.
  • Mention fence versioning when businesses update boundaries because in-flight trips and active evaluations require a consistent fence snapshot.
  • Common pitfall: brute-force checking every fence globally fails at scale; using a hierarchical spatial index is non-negotiable.

Engineering Trade-offs

Spatial Indexing Architecture: R-tree vs Geohash Grid vs Quadtree

Grid indexes, bounding volume hierarchies, and adaptive quadtrees offer distinct trade-offs across query throughput, memory overhead, and irregular polygon support. Compare with spatial partitioning patterns in Foursquare Check-ins.

IndexCharacteristicsMemoryBest For
R-tree ⭐Balanced tree of bounding rectangles, great for overlapping polygons10 GB for 100MGeneral purpose, PostGIS
Geohash GridDivide world into grid cells, simpler, works with RedisDepends on precisionDistributed cache, simpler deployments
QuadtreeRecursive subdivision, adapts to densityDynamicNon-uniform distribution (dense + sparse)

In-Line vs Async Evaluation

In-Line: Synchronous check in request path → lowest latency but can't handle 5M/sec
Async (Kafka + Flink) ⭐: Decouple ingestion from evaluation → handles 5M/sec easily
  For safety-critical: use in-line path for high-priority fences

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