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)
| Question | Why 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.
| Metric | Calculation | Value |
|---|---|---|
| Active geofences | Given (assumption documented in value) | 100M |
| Active devices | Given (assumption documented in value) | 10M concurrently |
| Location updates / sec | From Location updates / day ÷ 86400 (+ peak factor in value) | 5M peak |
| Avg geofence polygon vertices | Given (typical workload assumption) | 6 |
| Geofence storage per fence | Given | ~500 bytes |
| Total geofence data | 100M x 500B | 50 GB |
| Trigger events / sec | Given | ~50K |
| Event notification delivery | Given (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.
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 stateEvent 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 > 30sAPI 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
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
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
GET /api/v1/geofences/{fence_id}/events?start=2025-03-14T00:00:00Z&end=2025-03-14T23:59:59ZQuery Active Devices
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
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} → INTClickHouse: Event Analytics
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
| Concern | Solution |
|---|---|
| Evaluation engine failure | Flink checkpointing and consumer group rebalance |
| Spatial index crash |
|
| Redis state loss | Redis Cluster with AOF persistence |
| Webhook delivery failure |
|
| Duplicate triggers | Idempotent processing: deduplicate by (device, fence, type, 5-min window) |
| GPS accuracy poor | Ignore 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.
| Index | Characteristics | Memory | Best For |
|---|---|---|---|
| R-tree ⭐ | Balanced tree of bounding rectangles, great for overlapping polygons | 10 GB for 100M | General purpose, PostGIS |
| Geohash Grid | Divide world into grid cells, simpler, works with Redis | Depends on precision | Distributed cache, simpler deployments |
| Quadtree | Recursive subdivision, adapts to density | Dynamic | Non-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
How helpful was this walkthrough?
Click a star to rate. We actively use this feedback to refine and update our system design content.
Discussion
Share your thoughts, ask questions, or help others.