Routing Spatial Queries by Bounding-Box Locality
Header-based routing answers “which domain”, and for many spatial requests that is not enough: a query bounded to a region should reach the replica nearest that region’s data, not whichever replica the load balancer picked. Bounding-box locality routing adds that second decision — resolve the extent to a spatial partition, then prefer the replica holding it — turning cross-region reads into local ones. This guide implements it without introducing the non-determinism that makes routing undebuggable. It extends the routing model in Cross-Domain Routing Strategies within Federated Ownership & Routing Architecture, and it composes with Header-Based Routing for Spatial Domains.
Prerequisites
| Requirement | Value / Assumption | Notes |
|---|---|---|
| Tools | The gateway with a request-time extension point, python3 ≥ 3.11 |
The mapping must be cheap and side-effect free |
| Partitioning | Domain data partitioned by a spatial key, replicas aligned to it | Locality is meaningless without it |
| CRS convention | Request bbox declared in EPSG:4326 |
A bbox in another CRS resolves to the wrong partition |
| Residency | Residency evaluated before locality | Locality must never override a lawful-region constraint |
| Environment | PARTITION_MAP, GATEWAY_ENV |
Exported before running |
Step-by-Step Implementation
1. Build a static, cheap extent-to-partition map
The mapping runs on every request, so it must be a lookup rather than a query. A precomputed grid is enough and is trivially deterministic.
# locality.py — extent to partition, with no I/O on the request path.
import math
GRID_DEG = 5.0 # coarse cells; the map stays small enough to hold in memory
def cells_for_bbox(bbox: tuple[float, float, float, float]) -> set[str]:
"""Every grid cell a bbox touches. Coarse deliberately: the goal is to pick a
replica, not to plan a query, and a fine grid makes the map large for no gain."""
minx, miny, maxx, maxy = bbox
cells = set()
x = math.floor(minx / GRID_DEG) * GRID_DEG
while x <= maxx:
y = math.floor(miny / GRID_DEG) * GRID_DEG
while y <= maxy:
cells.add(f"{int(x)}:{int(y)}")
y += GRID_DEG
x += GRID_DEG
return cells
def preferred_replicas(bbox, partition_map: dict, replicas: dict) -> list[str]:
"""Replicas holding the partitions this bbox touches, most-covering first.
Deterministic: same bbox and same map always give the same ordering."""
partitions = {p for c in cells_for_bbox(bbox) for p in partition_map.get(c, ())}
scored = [
(len(partitions & set(held)), name)
for name, held in replicas.items()
]
# Sort by coverage descending, then by name — never by a random tiebreak, or
# two identical requests take different paths and nothing is reproducible.
scored.sort(key=lambda s: (-s[0], s[1]))
return [name for score, name in scored if score > 0]
Verify the mapping is deterministic and fast enough for the hot path:
python3 - <<'PY'
import locality, timeit
m = {"−5:50": ("eu-west",), "0:50": ("eu-west", "eu-central")}
r = {"eu-west": ("eu-west",), "eu-central": ("eu-central",)}
bbox = (-1, 50, 1, 52)
a = locality.preferred_replicas(bbox, m, r)
b = locality.preferred_replicas(bbox, m, r)
assert a == b, "non-deterministic ordering"
print(a, f"{timeit.timeit(lambda: locality.preferred_replicas(bbox, m, r), number=10000)/10000*1e6:.1f} µs")
PY
2. Apply locality after residency, never before
Locality is an optimisation; residency is a constraint. Evaluating them in the wrong order produces a fast, unlawful route.
# route.py — the ordering that keeps locality safe.
def select_backend(request, product, replicas) -> str:
# 1. Residency eliminates unlawful replicas outright. Non-negotiable.
lawful = [r for r in replicas if r.region in product.allowed_regions]
if not lawful:
raise Unroutable("451: no replica in a permitted region")
# 2. Health removes replicas that cannot serve, whatever their locality.
healthy = [r for r in lawful if r.healthy]
if not healthy:
raise Unroutable("503: no healthy replica in a permitted region")
# 3. Locality orders what remains. An optimisation, applied last.
if request.bbox:
preferred = preferred_replicas(request.bbox, PARTITION_MAP,
{r.name: r.partitions for r in healthy})
for name in preferred:
return name
# 4. No bbox, or no replica holds the partition: ordinary load balancing.
return least_loaded(healthy).name
Verify residency wins when the two disagree — the test that matters:
curl -sS -o /dev/null -D - \
-H "x-spatial-domain: cadastre" \
-H "x-spatial-bbox: -1,50,1,52" \
"https://api.internal/collections/parcels/items" \
| grep -iE '^(x-served-by|x-served-region):'
# Must be a region in the product's allowed set, even where a nearer one exists.
3. Make the decision explainable offline
A routing decision that cannot be reproduced without live state is a routing decision nobody can debug.
# Same inputs, same answer, no cluster required.
gateway-cli explain-route \
--domain cadastre --product parcels \
--bbox " -1,50,1,52" --crs EPSG:4326 \
--partition-map "$PARTITION_MAP" --replicas /tmp/replicas.json
# residency: eu-west, eu-central permitted; us-east excluded
# health: eu-west healthy, eu-central healthy
# locality: eu-west covers 2/2 partitions -> selected
4. Measure whether locality is actually paying
# Cross-region read share should fall after enabling locality. If it does not, the
# partition map does not match how data is actually replicated.
curl -sS "$PROM/api/v1/query" --data-urlencode \
'query=sum(rate(backend_requests_total{cross_region="true"}[30m]))
/ sum(rate(backend_requests_total[30m]))' | jq -r '.data.result[0].value[1]'
Configuration Reference
| Parameter | Value | Effect |
|---|---|---|
GRID_DEG |
5.0 |
Coarse cells keep the map memory-resident |
| Evaluation order | Residency → health → locality | Locality must never override a constraint |
| Tiebreak | Coverage, then name | Never random; identical requests must route identically |
| Fallback | Ordinary load balancing | A bbox with no matching partition is not an error |
| Request bbox CRS | EPSG:4326, declared |
Another CRS resolves to the wrong cells |
| Map source | Precomputed, deployed with the table | No I/O on the request path |
Common Failure Modes & Fixes
Two identical requests reach different replicas. Root cause: a random or load-based tiebreak inside the locality step. Fix: sort deterministically; locality decides preference, and load balancing only applies when locality expresses none.
Cross-region reads do not fall after enabling it. Root cause: the partition map does not reflect actual replication — usually because replicas hold everything rather than partitions. Fix: locality is only meaningful with partition-aligned replicas; without them, disable it rather than paying the lookup for nothing.
A request is routed to a lawful-but-distant replica. Root cause: correct behaviour when the near replica is outside the permitted region. Fix: none; this is residency winning, which is the required ordering.
Latency improves for most requests and worsens for some. Root cause: locality is concentrating load on the replica holding a popular partition. Fix: cap the share locality may direct to one replica, falling back to load balancing above it.
A bbox in the wrong CRS routes oddly.
Root cause: EPSG:3857 coordinates interpreted as degrees resolve to a cell far from the intended region. Fix: require the CRS header and reject a bbox whose values fall outside the declared CRS’s domain.
FAQ
Is this not just data locality by another name?
It is, applied at the routing layer rather than inside the storage engine — and that placement is what makes it useful in a federated estate. A database can only optimise locality within itself; the routing plane sits above several domains, each with its own replication topology, and is the only component positioned to choose between them. The trade is that the routing plane needs a map of where partitions live, which is why the map is precomputed and deployed alongside the route table rather than queried live.
How coarse should the grid be?
Coarse enough that the map fits comfortably in memory and never needs a lookup off-box. Five-degree cells over a global estate produce a few thousand entries, which is trivial; half-degree cells produce hundreds of thousands and buy almost nothing, because the decision being made is “which replica” and replicas hold large regions. The grid only needs to be fine enough to distinguish replicas, not to describe the data.
What happens for a query with no bounding box?
It falls through to ordinary load balancing, which is correct. A request without a spatial bound has no locality to exploit, and inventing one — from the caller’s region, say — would route by who is asking rather than by what is being asked for. That is a different optimisation with different trade-offs, and conflating the two makes both harder to reason about.
Does locality routing interact with caching?
Yes, and usually favourably: directing requests for a region to a consistent replica improves that replica’s cache hit ratio for the same reason it improves data locality. The caution is the concentration failure above — locality plus caching can make one replica very hot for a popular region, at which point the cap on locality’s share becomes load-bearing rather than theoretical.
How often should the partition map be regenerated?
Whenever replication topology changes, and on a slow schedule otherwise. The map is a description of where partitions live, so it goes stale when a replica is added, removed, or rebalanced — and a stale map does not break routing, it merely stops helping, because a preferred replica that no longer holds the partition falls through to ordinary load balancing. That graceful degradation is deliberate: it means a forgotten regeneration costs some locality rather than causing an outage. Deploying the map alongside the route table, which is already regenerated on every registry change, keeps the two in step without a separate process to remember.
Related
- Cross-Domain Routing Strategies — the parent topic and the precedence rules
- Header-Based Routing for Spatial Domains — the domain decision this refines
- Generating Gateway Routes from Domain Manifests — where the partition map is deployed alongside the table