Low Level Design: Geo-Fencing Service

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-updates topic 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: Netflix Interview Guide 2026: Streaming Architecture, Recommendation Systems, and Engineering Excellence

See also: Airbnb Interview Guide 2026: Search Systems, Trust and Safety, and Full-Stack Engineering

Scroll to Top