Data Contracts for Spatial Products
A data contract for a spatial product is the machine-checkable agreement that pins down five things at once — schema, coordinate reference system, geometry precision, service-level guarantees, and semantics — and enforces them at the exact boundary where a producing domain hands data to a consuming one. This reference sits inside Geospatial Data Mesh Fundamentals and treats the contract as executable infrastructure, not a wiki page: if a producer changes its CRS from EPSG:4326 to EPSG:3857, drops a required attribute, or coarsens geometry precision below the promised tolerance, the boundary rejects the payload before any consumer resolves it. Platform engineers and GIS data stewards should read this alongside Schema Contracts for Vector Tile Data, which applies the same discipline to the tile-serving layer, and Product Thinking for GIS Datasets, which supplies the ownership and SLA model that a contract encodes. Where the two diverge, the version stamp v1.2.0-crs:EPSG:4326-res:10m is the arbiter — it is the immutable identity a consumer pins to.
Figure — A spatial data contract mediates the producer/consumer boundary, checking schema, CRS, geometry precision, SLA, and semantics before stamping an immutable version on the payload.
Architectural Boundaries & Design Rationale
The contract exists because a federated mesh removes the single geodatabase that once forced every producer and consumer to share one implicit schema. Once each domain owns and publishes independently, “the data changed and nothing told me” becomes the dominant failure mode. A data contract restores the missing signal by making the boundary itself refuse any product that no longer matches what consumers agreed to depend on. It is enforced at ingress — the same zero-trust posture the catalog applies in Metadata Cataloging for Raster/Vector — so a non-conforming payload never reaches storage, discovery, or a downstream join.
Five surfaces make up the contract, and each maps to a distinct class of silent breakage:
- Schema — attribute names, types, nullability, and cardinality. Removing a required field or narrowing a type breaks every consumer that projected it.
- CRS — the declared
crs_epsgand axis order. A producer that flips fromEPSG:4326toEPSG:32633, or serves lat/lon where consumers expect lon/lat, corrupts every spatial predicate downstream. The governance model for this surface lives in CRS Governance and Reprojection Standards. - Geometry precision — coordinate resolution and the topology rule set. A product promising sub-metre planar topology that ships decimated geometry fails validation rather than quietly degrading a routing graph.
- SLA — freshness, availability, and latency targets. These are the guarantees a consumer plans capacity against, drawn from the same model as Product Thinking for GIS Datasets.
- Semantics — units, enumerations, null conventions, and the meaning of each field. A
populationcolumn that switches from count to density is schema-valid and semantically catastrophic.
The rationale for enforcing all five at one gate rather than scattering checks across pipelines is idempotency and single-point accountability: a payload validated once against a pinned contract version behaves identically on every retry, and a breach has exactly one place to page. Contract evolution is governed by Versioning Spatial Data Contracts with SemVer, which decides whether a change is a compatible MINOR bump or a breaking MAJOR that forces a parallel version.
Specification & Contract Reference
A spatial data contract is a versioned document, stored beside the product and referenced by its stamp. The mandatory fields below are the minimal surface every producer must publish and every consumer may rely on.
| Field | Type | Required | Constraint / Convention |
|---|---|---|---|
contract_version |
string | yes | v1.2.0-crs:EPSG:4326-res:10m site convention; immutable once published |
product_id |
UUID | yes | Stable identity across contract versions |
crs_epsg |
integer | yes | EPSG code; change is always a MAJOR bump |
axis_order |
enum | yes | lon_lat | lat_lon; explicit to avoid silent inversion |
geometry_type |
enum | yes | point | linestring | polygon | multipolygon | raster |
coordinate_precision |
integer | yes | Decimal places retained; drives precision validation |
topology_rule_set |
enum | vector only | none | planar | network | 3d_solid |
schema_fields |
array | yes | Ordered {name, type, nullable, unit} records |
freshness_sla_minutes |
integer | yes | 0 means real-time; drives drift alerts |
availability_target |
number | yes | Fractional, e.g. 0.999; consumer capacity input |
semantics |
object | yes | Per-field units, enums, and null meaning |
product_owner_id |
yes | Accountable steward for breaches and incidents |
The enforcement surface — where the contract is evaluated — is carried in transport and policy inputs rather than the payload body, so the same contract can gate a REST ingress, a stream consumer, or a CI check.
| Surface | Key | Purpose |
|---|---|---|
| HTTP header | X-Contract-Version |
Pins the exact contract the payload claims to satisfy |
| Policy input | input.contract.crs_epsg |
Validated against the CRS allowlist and the pinned version |
| Policy input | input.payload.schema_hash |
Detects drift between declared and actual schema |
| Idempotency key | SHA-256(product_id ‖ contract_version ‖ payload_hash) |
Makes re-validation of the same payload side-effect free |
| Registry entry | contract://<product_id>/<contract_version> |
Canonical, immutable storage path for the contract document |
Consumers subscribe to a specific contract_version and are notified only when a compatible MINOR/PATCH supersedes it; a MAJOR requires an explicit, tested migration rather than an automatic upgrade.
Production Implementation
The contract is authored as a machine-readable document and enforced by a policy engine at the boundary. Below is a data-contract schema in YAML (the human-authored source) and its JSON Schema projection used for structural validation, followed by an opa eval gate that checks the cross-cutting rules JSON Schema cannot express — CRS allowlisting, version pinning, and ownership. The gate is idempotent and zero-trust by default: identity is verified upstream, default allow = false denies anything not explicitly permitted, and re-evaluating the same payload against the same contract version always yields the same decision.
# contract://parcels_cadastre/v1.2.0 — authored source of truth
contract_version: "v1.2.0-crs:EPSG:4326-res:10m"
product_id: "7c9e6a1b-2f34-4d8a-9b11-0a5c2e7f4d21"
crs_epsg: 4326
axis_order: lon_lat
geometry_type: multipolygon
coordinate_precision: 6 # ~0.11 m at the equator
topology_rule_set: planar
schema_fields:
- { name: parcel_id, type: string, nullable: false, unit: none }
- { name: area_m2, type: number, nullable: false, unit: square_metre }
- { name: land_use, type: string, nullable: false, unit: enum }
freshness_sla_minutes: 1440
availability_target: 0.999
semantics:
land_use: { enum: [residential, commercial, agricultural, industrial] }
area_m2: { null_means: not_applicable }
product_owner_id: cadastre-steward@example.org
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SpatialDataContract",
"type": "object",
"required": ["contract_version", "product_id", "crs_epsg", "axis_order",
"geometry_type", "coordinate_precision", "schema_fields",
"freshness_sla_minutes", "availability_target", "product_owner_id"],
"properties": {
"contract_version": { "type": "string", "pattern": "^v\\d+\\.\\d+\\.\\d+-crs:EPSG:\\d{4,5}-res:.+$" },
"product_id": { "type": "string", "format": "uuid" },
"crs_epsg": { "type": "integer", "minimum": 1000, "maximum": 99999 },
"axis_order": { "type": "string", "enum": ["lon_lat", "lat_lon"] },
"geometry_type": { "type": "string",
"enum": ["point", "linestring", "polygon", "multipolygon", "raster"] },
"coordinate_precision": { "type": "integer", "minimum": 0, "maximum": 15 },
"topology_rule_set": { "type": "string", "enum": ["none", "planar", "network", "3d_solid"] },
"schema_fields": { "type": "array", "minItems": 1 },
"freshness_sla_minutes": { "type": "integer", "minimum": 0 },
"availability_target": { "type": "number", "exclusiveMinimum": 0, "maximum": 1 },
"product_owner_id": { "type": "string", "format": "email" }
},
"additionalProperties": false
}
package contract.boundary
import rego.v1
default allow = false
# Zero-trust: the payload is admitted only if it pins the published contract
# version, declares an allowlisted CRS, and its schema hash matches the contract.
allow if {
input.header["x-contract-version"] == input.contract.contract_version
input.contract.crs_epsg in [4326, 3857, 32610, 32611, 32612, 32633]
input.payload.schema_hash == input.contract.schema_hash
input.payload.coordinate_precision >= input.contract.coordinate_precision
}
Run the JSON Schema check and the policy together at ingress. In CI, opa eval -i request.json -d boundary.rego "data.contract.boundary.allow" fails the producer’s merge before a breaking payload is ever published; at runtime the same bundle runs as an admission sidecar so the boundary, the registry, and the consumer all enforce the identical contract independently. A false decision returns 409 Conflict with the offending field, and the producer must either fix the payload or, if the change is intentional, cut a new contract version.
Diagnostic Runbook
When a consumer reports broken or empty results downstream, isolate the failing contract surface in order from transport to semantics.
- Version pin mismatch (HTTP 409). Compare the
X-Contract-Versionheader against the registry entry the consumer subscribed to. A mismatch means a producer published a new version without a compatible migration. Remediation: re-tag the payload with the pinned version, or coordinate aMAJORmigration per Versioning Spatial Data Contracts with SemVer. - Schema-hash drift. If
input.payload.schema_hashdiffers frominput.contract.schema_hash, the producer changed columns without bumping the contract. Remediation: diff the field list, classify the change, and republish under the correct version bump. - CRS or axis-order rejection. An
allow = falsewith a matching version usually meanscrs_epsgfell outside the allowlist oraxis_orderwas inverted. Remediation: reproject at the boundary per CRS Governance and Reprojection Standards; never widen the allowlist to force a pass. - Precision violation.
coordinate_precisionbelow the contract minimum fails the policy. Remediation: stop decimating geometry upstream, or bump the contract if the coarser precision is a deliberate, communicated change. - Semantic breakage that passes schema. Values are schema-valid but wrong (units flipped, enum extended). Remediation: add the changed enum/unit to the contract
semanticsblock and treat an incompatible change asMAJOR. - Idempotency collision. The same
product_idandcontract_versionproduced two divergent payload hashes. Remediation: return the existing record for identical payloads; reject divergent ones with409and require a version bump. - SLA breach without a schema fault. Data is valid but stale or unavailable. Remediation: page the
product_owner_id, trigger a producer re-run, and check the freshness monitor againstfreshness_sla_minutes.
SLA Targets & Performance Baselines
Contract enforcement is itself a product surface with guarantees. These baselines apply per boundary and feed the alerting that governs remediation.
| Metric | Target | Alert Threshold | Remediation Action |
|---|---|---|---|
| Contract validation latency (p95) | < 30 ms | > 90 ms for 10 min | Cache compiled policy bundle; co-locate the gate sidecar |
| Breaking-change escape rate | 0 | any breach reaching a consumer | Add the missing rule; block on CI, not runtime |
| Schema-drift detection lag | < 1 publish cycle | drift found post-publish | Move the hash check into the producer’s merge gate |
| Contract-version pin coverage | 100% of consumers | any unpinned consumer | Require X-Contract-Version; deny unpinned reads |
Freshness vs freshness_sla_minutes |
within SLA | 2× SLA exceeded | Re-run producer pipeline; page owner |
| Idempotent re-validation | 100% identical decisions | any nondeterministic result | Pin policy bundle version; remove clock/random inputs |
What Belongs in the Contract, and What Must Not
A contract is only useful if both sides can rely on it, which means every clause has to be something the producer can genuinely commit to and the consumer can genuinely verify. Contracts fail in two symmetrical ways: they promise things the producer cannot control, and they omit things the consumer silently depends on.
The clauses that belong are those that are observable from the output port alone. A consumer can verify a geometry type, a CRS, an attribute’s presence and type, a coordinate precision, a bounding box, a resolution, and an update cadence, all without any knowledge of how the product is made. Each of these can be asserted mechanically at request time and each of them, if violated, breaks a consumer in a way that is attributable. These are the load-bearing clauses.
| Clause | Belongs in the contract because | Verified by |
|---|---|---|
| Native CRS | Determines what every coordinate means | Reading the manifest; comparing against a known control point |
| Geometry type & dimensionality | A consumer’s spatial predicates depend on it | Schema validation at the boundary |
| Attribute schema & nullability | Drives every downstream join and filter | Contract test against a sample response |
| Coordinate precision | Sets the resolution at which equality is meaningful | Inspecting returned coordinates |
| Declared extent | Distinguishes “no data here” from “outside coverage” | Comparing a query bbox against the manifest |
| Resolution / scale | Determines fitness for a consumer’s use | Metadata; measured feature density |
| Update cadence | The basis of every freshness expectation | Observed publication timestamps |
| Availability & latency SLO | What the consumer designs their own SLO against | The producer’s published indicators |
The clauses that do not belong are internal mechanisms. Storage format, indexing strategy, partitioning scheme, tile pyramid internals, the database product, the orchestrator — none of these should appear in a contract, because committing to them removes the producer’s freedom to improve without a breaking change, and because a consumer who depends on them has coupled to something the boundary was supposed to hide. A contract that specifies “features are stored in PostGIS with a GiST index” has converted an implementation choice into a promise, and the first time the domain moves to a columnar store it will be a MAJOR version bump for no consumer-visible reason.
There is a third category worth naming explicitly, because it causes the most trouble: things consumers depend on that nobody wrote down. Feature ordering is the classic example — a consumer builds a pipeline that assumes results arrive in a stable order, the producer changes an index, and the consumer breaks with no contract having been violated. Others include the presence of a particular attribute value, the maximum number of features in a response, the behaviour at the antimeridian, and whether an empty result is a 200 with zero features or a 404. The remedy is not to enumerate every possible dependency in advance; it is to state the negative clauses explicitly. A contract that says “feature order is unspecified and will change” gives the consumer a fair warning and the producer a defensible position, and it costs one line.
Finally, a contract must state what happens when it cannot be met. A product that is unavailable, stale, or degraded is in a state the contract should describe — which response a consumer receives, how degradation is labelled, and how long the producer commits to holding the previous version live during a deprecation. A contract that only describes the happy path leaves the most consequential moments undefined, which is exactly when both sides most need to know what to expect.
One practical note on where the contract lives. A contract that exists only as a document describing an endpoint will drift from the endpoint within a release or two, because nothing forces them to agree. The arrangement that holds is for the machine-readable contract to be the artifact from which both the boundary validator and the published documentation are generated — so a clause that is not enforced cannot be published, and a clause that is enforced cannot be omitted from the documentation. Where that generation step exists, contract drift stops being a discipline problem and becomes a build failure.
Governance & Compliance Notes
Every contract version is immutable and append-only. Publishing v1.3.0 never mutates v1.2.0; the older document stays queryable for a defined retention window (typically 90 days) so audit and rollback can reconstruct exactly which contract a historical payload satisfied. Each boundary decision emits an audit event capturing the actor, the product_id, the resolved contract_version, the policy result, and the offending field on denial — the same evidentiary discipline the catalog applies at registration. The CRS allowlist in the Rego gate is the single enforcement point for approved projections, so an unapproved crs_epsg cannot enter the mesh regardless of which producer publishes it. Ownership is non-negotiable: a contract without an accountable product_owner_id is itself invalid, because a breach with no steward has no path to resolution. Managed this way, the contract becomes the durable interface of a spatial product — consumers plan against it, producers evolve behind it, and the version stamp guarantees the two never drift apart silently.
Related
- Geospatial Data Mesh Fundamentals — parent reference and architectural overview
- Versioning Spatial Data Contracts with SemVer — how contract versions evolve without breaking consumers
- Schema Contracts for Vector Tile Data — the same discipline applied at the tile-serving layer
- Product Thinking for GIS Datasets — the ownership and SLA model a contract encodes
- CRS Governance and Reprojection Standards — how the CRS surface of the contract is governed across domains