Geo-Fencing Service: Low Level Design
Data Model
GeoFence Table
GeoFence
--------
id BIGINT PK
name VARCHAR(255)
type ENUM('circle','polygon')
center_lat DOUBLE -- circles only
center_lng DOUBLE -- circles only
radius_meters INT -- circles only
geom GEOGRAPHY(POLYGON) -- PostGIS, used for all shapes
owner_id BIGINT
trigger_on ENUM('enter','exit','both')
active BOOLEAN
created_at TIMESTAMPTZ
Circles are also stored as polygon geometries (approximated as a 64-point polygon) to unify spatial queries, but center_lat, center_lng, and radius_meters are retained for a fast Haversine pre-filter before invoking PostGIS.
Event Table
FenceEvent
----------
id BIGINT PK
fence_id BIGINT FK
device_id BIGINT
event_type ENUM('enter','exit')
occurred_at TIMESTAMPTZ
GeoFenceSubscription Table
GeoFenceSubscription
--------------------
id BIGINT PK
fence_id BIGINT FK
callback_url TEXT
secret VARCHAR(64) -- HMAC signing key for webhook payloads
created_at TIMESTAMPTZ
Location Update Ingestion
API Endpoint
POST /locations
Content-Type: application/json
{
"updates": [
{ "device_id": 42, "lat": 37.7749, "lng": -122.4194, "ts": 1700000000 },
...
]
}
Accepts 1–10 location updates per device per second. Updates are written to a Kafka topic location-updates for async fence evaluation, keeping the HTTP response fast. The ingestion service validates coordinate ranges and rate-limits per device.
Fence Evaluation Pipeline
Spatial Query
-- Find all active fences containing the new device location
SELECT gf.id, gf.trigger_on
FROM geofence gf
WHERE gf.active = TRUE
AND gf.owner_id IN (/* fences relevant to this device */)
AND ST_Within(
ST_SetSRID(ST_MakePoint(:lng, :lat), 4326)::geography,
gf.geom
);
Spatial Index
CREATE INDEX idx_geofence_geom ON geofence USING GIST (geom);
The GiST index enables a bounding-box pre-filter so PostGIS only runs the precise ST_Within check against a small candidate set. For circle fences, a Haversine distance check runs first as an additional pre-filter before the PostGIS call.
State Tracking and Debounce
To prevent false events from GPS jitter, a device's inside/outside state per fence is tracked in Redis:
Key: fence_state:{fence_id}:{device_id}
Value: { "state": "inside", "since": 1700000000 }
TTL: 300s
An enter event is only fired when the device has been continuously inside the fence for >30 seconds. An exit event is only fired when the device has been continuously outside for >30 seconds. This debounce window is configurable per fence.
Webhook Delivery
When a confirmed enter/exit event is written to FenceEvent, the system looks up all GeoFenceSubscription rows for that fence and enqueues a webhook delivery task:
POST {callback_url}
X-Signature: HMAC-SHA256(secret, payload)
Content-Type: application/json
{
"fence_id": 101,
"device_id": 42,
"event_type": "enter",
"occurred_at": "2024-01-01T12:00:00Z"
}
Delivery uses exponential backoff with up to 5 retries. Failed deliveries are logged for manual inspection.
Scalability Notes
- Fence evaluation workers consume from the Kafka
location-updatestopic and scale horizontally. - Active fence sets per device are cached in Redis to avoid per-update DB lookups.
- PostGIS runs on a read replica; writes go to primary.
- Very high-volume deployments can shard by geographic region using a geohash prefix.
See also: Uber Interview Guide 2026: Dispatch Systems, Geospatial Algorithms, and Marketplace Engineering
See also: Airbnb Interview Guide 2026: Search Systems, Trust and Safety, and Full-Stack Engineering