Migrating from monolithic GIS to data mesh

Cutting a production estate over from a centralized PostGIS/ArcSDE geodatabase to a federated set of owned spatial products is the one operation where a botched routing change silently corrupts every downstream query at once. This guide is a concrete migration runbook under Data Mesh vs Traditional GIS Architecture, itself part of Geospatial Data Mesh Fundamentals: it walks through standing up a deterministic routing layer that intercepts, validates, and forwards spatial queries only after strict schema and catalog parity are proven, so the monolith can be decomposed domain by domain without spatial index fragmentation or metadata desynchronization. Every domain you carve out must already have its ownership lines drawn through Spatial Domain Boundary Design; this page assumes those boundaries exist and focuses on the cutover mechanics that move traffic onto them safely.

Monolith-to-mesh cutover flow through the SpatialDomainRouter Spatial queries leave the monolithic PostGIS or ArcSDE geodatabase and enter a deterministic SpatialDomainRouter. The router applies three admission gates in sequence — CRS validation to EPSG:4326, schema validation of partition_key, geom and temporal_index, and catalog sync within a 120-second drift budget. Admitted traffic is routed to per-domain spatial products: hydrology, cadastre, and transport. If any gate trips, the circuit breaker opens and traffic falls back to the warm monolith-legacy-fallback route. Monolithic GIS PostGIS / ArcSDE GiST-indexed geom spatial queries SpatialDomainRouter deterministic admission gates 1 · CRS validation → EPSG:4326, reject mismatch 2 · Schema validation partition_key · geom · temporal 3 · Catalog sync drift < 120s or block traffic admitted domain-hydrology h3_resolution_7 domain-cadastre owned spatial product domain-transport owned spatial product circuit breaker open · any gate trips → monolith-legacy-fallback

Prerequisites

Requirement Value / Constraint
Source system PostGIS 3.4+ or ArcSDE geodatabase with GiST indexes on every geom column
Routing plane Kubernetes 1.28+ with server-side apply; Envoy or equivalent L7 proxy at ingress
Storage CRS Geometries canonicalized to EPSG:4326; web egress reprojected to EPSG:3857 only
Catalog Federated metadata catalog reachable at catalog-api.internal/v1 with sync-status endpoint
CLI tools kubectl, ogrinfo, psql, curl, jq, sha256sum, opa
Access roles platform-engineer (router deploy); GIS Data Steward for catalog reconciliation
Env vars DB_URL, CATALOG_TOKEN, CONFIG_HASH, BROKER_BOOTSTRAP

Step-by-Step Implementation

1. Map the dependency chain before any routing change

The routing layer is a state machine: traffic admission is gated by explicit architectural controls, and activating it before its dependencies are satisfied is what triggers silent spatial drift or timeout cascades. Confirm each control below resolves to a real, versioned artifact before you touch the route table. Product Thinking for GIS Datasets assigns routing accountability to domain product owners rather than central DBAs; Metadata Cataloging for Raster/Vector supplies the schema validation (geometry types, CRS, attribute nullability) that runs before traffic is admitted; and Scoping Rules for Spatial Products governs deterministic partition-key generation so spatial indexing stays uniform across boundaries.

bash
# Verify every domain product carries an immutable, versioned tag before cutover.
# Unversioned or mutable staging artifacts must never be routed to.
for domain in hydrology cadastre transport; do
  curl -s -H "Authorization: Bearer $CATALOG_TOKEN" \
    "https://catalog-api.internal/v1/spatial/products/${domain}" | \
    jq -r 'select(.schema_version | test("^v[0-9]+\\.[0-9]+\\.[0-9]+-crs:EPSG:[0-9]+-res:")) | .schema_version
           // "UNVERSIONED — block routing"'
done

Any product printing UNVERSIONED must be tagged — following the site convention v1.2.0-crs:EPSG:4326-res:10m — and its retention and archival triggers configured through Spatial Product Lifecycle Management before it is eligible for traffic.

2. Deploy the deterministic router idempotently

The routing layer is deployed as a declarative, idempotent manifest that enforces CRS normalization, partition alignment, and SLA circuit breaking at admission. Wrap the apply in an atomic state check so a partial activation can never leave half the domains routed.

yaml
# routing-layer-config.yaml
apiVersion: routing.mesh/v1
kind: SpatialDomainRouter
metadata:
  name: gis-domain-router
  annotations:
    idempotency-key: "sha256-placeholder-replace-at-deploy-time"
spec:
  admission_gates:
    crs_validation:
      target_crs: "EPSG:4326"
      fallback_transform: true
      reject_on_mismatch: true
    schema_validation:
      required_fields: ["partition_key", "geom", "temporal_index"]
      geometry_type: ["Polygon", "MultiPolygon", "Point"]
    catalog_sync:
      max_drift_seconds: 120
      block_traffic_on_desync: true
  circuit_breaker:
    error_threshold: 0.05
    timeout_ms: 3000
    fallback_route: "monolith-legacy-fallback"
  routing_rules:
    - match:
        header: "x-domain-scope: hydrology"
      route: "domain-hydrology-v1.4.2"
      partition_strategy: "h3_resolution_7"
bash
# Compute manifest hash for the audit trail, then apply server-side (idempotent).
CONFIG_HASH=$(sha256sum routing-layer-config.yaml | awk '{print $1}')
kubectl apply -f routing-layer-config.yaml \
  --server-side --field-manager=platform-engineer
kubectl rollout status deploy/gis-domain-router --timeout=300s
echo "Routing state locked: ${CONFIG_HASH}"

3. Run pre-cutover diagnostics against staging replicas

Validate partition alignment and catalog drift on a replica before admitting any production traffic. The three checks below isolate the failures that most often surface only after cutover.

bash
# Validate spatial partition alignment and CRS consistency.
ogrinfo -al -so /path/to/spatial_product_staging.gpkg | \
  grep -E "Geometry|SRS WKT|Feature Count"

# Identify non-WGS84 or invalid features in a PostGIS staging table.
psql "$DB_URL" -c "
SELECT partition_key, ST_SRID(geom) AS detected_srid, ST_IsValid(geom) AS valid
FROM spatial_product_staging
WHERE ST_SRID(geom) != 4326 OR NOT ST_IsValid(geom)
LIMIT 50;"

# Audit metadata catalog sync latency.
curl -s -H "Authorization: Bearer $CATALOG_TOKEN" \
  https://catalog-api.internal/v1/spatial/metadata/sync-status | jq '.drift_seconds'

If drift_seconds exceeds 120 or the PostGIS query returns rows, the router must keep blocking traffic until catalog parity is restored. Authoritative CRS transformation matrices live in the OGC Coordinate Reference Systems Registry, and partition-aligned geometry encoding follows the GeoParquet Specification v1.1.0.

4. Gate traffic admission behind circuit breakers

Circuit breakers operate at the query execution boundary, not the network layer, so they can read spatial join latency and catalog sync status atomically. Admission requires three concurrent conditions: drift_seconds < 120 across all domain catalogs, zero CRS_MISMATCH or PARTITION_DRIFT logs in the last 15-minute window, and a passing immutable-tag validation.

bash
# Refuse to shift traffic while the breaker is open.
CIRCUIT_STATE=$(curl -s http://router-internal:8080/health/circuit-breaker | jq -r '.state')
if [[ "$CIRCUIT_STATE" == "OPEN" ]]; then
  echo "Circuit OPEN: routing blocked, awaiting catalog sync stabilization."
  exit 1
fi

# Atomic, reversible traffic shift onto domain routing.
kubectl patch configmap gis-routing-flags \
  --patch '{"data":{"allow_domain_routing":"true"}}'

5. Cut over per domain and keep rollback one command away

Shift one domain at a time, watch the log patterns in Section Common Failure Modes, and confirm the active route before proceeding. The monolith-legacy-fallback route stays warm throughout, so rollback is a single idempotent patch. Contract-aware dispatch during the dual-run window is handled by Cross-Domain Routing Strategies, which reads each payload’s contract fingerprint to pick the correct domain.

bash
# Idempotent rollback to the legacy monolith and a diagnostic snapshot.
kubectl patch configmap gis-routing-flags \
  --patch '{"data":{"allow_domain_routing":"false"}}'
curl -s http://router-internal:8080/routing/status | jq '.active_route'
kubectl logs deploy/gis-domain-router --tail=5000 > /tmp/router-failure-$(date +%s).log

Do not reactivate domain routing until catalog parity, partition alignment, and schema validation all return zero anomalies.

Configuration Reference

Parameter Scope Value / Default Rationale
target_crs crs_validation EPSG:4326 Canonical storage CRS; mismatches transform or reject
reject_on_mismatch crs_validation true Blocks silent reprojection drift at admission
required_fields schema_validation partition_key, geom, temporal_index Minimum contract for a routable product
max_drift_seconds catalog_sync 120 Catalog parity budget; exceeded → block traffic
error_threshold circuit_breaker 0.05 Trip ratio over the rolling window
timeout_ms circuit_breaker 3000 Per-query execution ceiling before fallback
fallback_route circuit_breaker monolith-legacy-fallback Warm legacy path for instant rollback
partition_strategy routing_rules h3_resolution_7 Deterministic spatial partition keying
schema_version product tag v1.2.0-crs:EPSG:4326-res:10m Immutable, CRS- and resolution-pinned identifier

Common Failure Modes & Fixes

Monitor these regex patterns in centralized aggregation (Splunk/ELK); each maps to a specific cutover fault.

Failure Mode Log Pattern (Regex) Symptom → Root Cause → Fix
CRS mismatch CRS_MISMATCH.*expected=EPSG:4326.*actual=EPSG:\d{4,5} Joins offset → a domain ships a non-WGS84 product → reject the query, trigger the transform pipeline, re-tag the product
Partition drift PARTITION_DRIFT.*h3_cell_mismatch.*index_fragmentation Query latency spikes → spatial index fragmented during bulk vector-to-GeoParquet conversion → halt routing for that domain, rebuild the spatial index
Catalog desync CATALOG_SYNC_TIMEOUT.*drift_seconds>\d{3} Stale partition reads → catalog lag past the 120s budget → block traffic, invoke the reconciliation job with a GIS Data Steward
Schema violation SCHEMA_REJECT.*geometry_type_violation.*null_geometry Malformed batch admitted → producer emitted a null or out-of-contract geometry → drop the batch, notify the domain steward, quarantine the producer above 5% reject rate
Breaker stuck open CIRCUIT_OPEN.*duration_ms>6\d{5} All domain traffic on fallback >10m → unresolved drift or partition fault → force rollback, capture heap/trace dumps, escalate to architecture review

FAQ

Why route through a state machine instead of just repointing connection strings?

Repointing connection strings admits traffic the instant DNS or a config map changes, with no gate between a half-migrated product and its consumers. Modeling admission as a state machine means CRS validation, schema parity, and catalog sync must all pass before a single query lands on a domain, so a partially converted GeoParquet partition or a desynced catalog can never serve corrupt results.

How do I migrate one domain without disturbing the others still on the monolith?

Keep the monolith-legacy-fallback route warm and shift domains one x-domain-scope header at a time. The circuit breaker evaluates each domain independently, so a hydrology cutover that trips on partition drift halts only hydrology routing — cadastre and transport keep serving from whichever side they currently target.

What makes the rollback safe to run repeatedly?

The rollback is a single kubectl patch that flips allow_domain_routing to false, which is idempotent: applying it when routing is already disabled is a no-op. Because the legacy route never went cold during the dual-run window, traffic returns to the monolith without a cache warm-up or index rebuild.

When is it safe to decommission the monolith?

Only after every domain has sustained zero CRS_MISMATCH, PARTITION_DRIFT, and CATALOG_SYNC_TIMEOUT events across a full business cycle and drift_seconds has held under 120 for all catalogs. Until then the fallback route is load-bearing, and its absence would remove your only single-command rollback.