Building a Golden Path Template for New Domains
A golden path that lives in documentation decays; one that lives in a template repository cannot decay without failing visibly. This guide builds that template: a repository a new spatial domain clones, fills in three values, and runs — producing a provisioned store, a validated product, a registered catalog entry, and a working tile port inside a day. It makes concrete the capability model in Self-Serve Platform Capabilities for Spatial Teams within Geospatial Data Mesh Fundamentals, and the infrastructure it stands up is covered in Provisioning Spatial Infrastructure with Terraform.
Prerequisites
| Requirement | Value / Assumption | Notes |
|---|---|---|
| Tools | git ≥ 2.40, python3 ≥ 3.11, make, the platform CLI |
No domain-specific tooling |
| CRS convention | Template defaults to EPSG:4326 storage, WebMercatorQuad tiles |
Deviating requires a stated reason |
| Access roles | domain-owner for the new domain; no platform-team involvement |
If a platform engineer is needed, the path is broken |
| CI | A pipeline that runs the template’s own checks on every commit | The template validates itself |
| Environment | DOMAIN, PRODUCT, OWNER_TEAM |
The only three values a new domain supplies |
The three-variable constraint is the design goal, not a simplification for this guide. Every additional value a new domain must decide is a decision they are not yet equipped to make, and each one lengthens time-to-first-product measurably.
Step-by-Step Implementation
1. Structure the template around one manifest and one command
The repository’s shape is itself the teaching. A domain engineer opening it should see immediately where their data is described and how it gets published, with no directory whose purpose needs explaining.
# The whole template. Nothing here is optional and nothing is a placeholder to fill in
# beyond the three values in product.yaml.
spatial-domain-template/
├── product.yaml # the entire declaration — CRS, ports, quality, SLO
├── Makefile # make plan | make apply | make publish | make verify
├── pipeline/
│ ├── ingest.py # source → canonical EPSG:4326, with the CRS declared
│ └── quality.sql # the blocking gate, unmodified from the platform default
├── tests/
│ └── test_contract.py # consumer-driven contract test, runs in CI
└── .github/workflows/
└── publish.yml # plan on PR, apply and publish on merge
Verify the template stands up unmodified before a domain ever touches it:
git clone https://git.internal/platform/spatial-domain-template demo-domain
cd demo-domain && DOMAIN=demo PRODUCT=sample OWNER_TEAM=platform make plan
# A template that cannot plan from a clean clone is already broken.
2. Make the defaults opinionated and the deviations explicit
Every value in the manifest that a domain could reasonably leave alone should already be filled in with the platform’s opinion, and every deviation should require a comment explaining it.
# product.yaml — the three variables are at the top; everything below is the opinion.
apiVersion: mesh.geospatial/v1
kind: SpatialProduct
metadata:
domain: ${DOMAIN} # ─┐
name: ${PRODUCT} # ├─ the only three values a new domain supplies
owner: ${OWNER_TEAM} # ─┘
spec:
storage:
class: postgis
crs: "EPSG:4326" # canonical; deviate only for survey-grade metric work
retention: P7Y
source:
crs: "REPLACE_ME" # must be declared — the pipeline fails closed without it
transformation: "REPLACE_ME" # pinned; never auto-selected
quality:
blocking: [geometry_validity_rate, crs_conformance, coordinate_precision]
precision_bound: 6
ports:
- type: tiles
matrixSet: WebMercatorQuad
zoom: { min: 6, max: 14 } # extend upward only where demand is measured
slo:
availability: 0.999
latency_p95_ms: 300
freshness: P1D
access:
principals: [] # default-deny; add principals deliberately
spatialScope: extent
Verify that the two REPLACE_ME values genuinely block rather than defaulting:
make plan 2>&1 | grep -q 'source.crs must be declared' \
&& echo "fails closed on an undeclared source CRS"
3. Make the Makefile the only interface
A domain should never need to know which tool runs underneath. Four targets cover the entire lifecycle, and each one is safe to run repeatedly.
# Makefile — the platform's whole surface for a domain team.
.PHONY: plan apply publish verify
plan: ## Show what provisioning would change. Read-only, always safe.
mesh-platform apply --manifest product.yaml --dry-run
apply: ## Reconcile infrastructure toward the manifest. Idempotent.
mesh-platform apply --manifest product.yaml
publish: ## Run the pipeline and publish a partition. Gated on quality.
python3 pipeline/ingest.py --manifest product.yaml
psql -f pipeline/quality.sql -v product=$(PRODUCT) -v partition=$(PARTITION)
mesh-platform publish --manifest product.yaml --partition $(PARTITION)
verify: ## Prove the product is discoverable, routable and within SLO.
mesh-platform verify --manifest product.yaml --check catalog,routing,slo
Verify idempotency, which is what makes the path safe for someone learning it:
make apply && make apply | tee /tmp/second.log
grep -q '0 changes' /tmp/second.log && echo "second apply is a no-op"
4. Have the template test itself in CI
A golden path that is not exercised on every change is a golden path that has already diverged from reality.
# .github/workflows/publish.yml — the template proves itself, continuously.
name: publish
on: [pull_request, push]
jobs:
golden-path:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Plan from a clean clone
run: DOMAIN=ci PRODUCT=smoke OWNER_TEAM=platform make plan
- name: Apply against the ephemeral environment
run: DOMAIN=ci PRODUCT=smoke OWNER_TEAM=platform make apply
- name: Confirm the second apply is a no-op
run: |
DOMAIN=ci PRODUCT=smoke OWNER_TEAM=platform make apply | tee out.log
grep -q '0 changes' out.log
- name: Publish a fixture partition and verify
run: |
DOMAIN=ci PRODUCT=smoke OWNER_TEAM=platform PARTITION=fixture make publish
DOMAIN=ci PRODUCT=smoke OWNER_TEAM=platform make verify
Verify the whole path end to end, timing it — the number that matters is elapsed time, not success:
time (make apply && PARTITION=fixture make publish && make verify)
# Under 20 minutes for a fresh domain is a working path; over an hour means a manual step.
Configuration Reference
| Field | Supplied by | Default | Deviation requires |
|---|---|---|---|
metadata.domain |
The new domain | none | — |
metadata.name |
The new domain | none | — |
metadata.owner |
The new domain | none | — |
source.crs |
The new domain | none — fails closed | — |
source.transformation |
The new domain | none — fails closed | — |
storage.crs |
Template | EPSG:4326 |
A survey-grade metric use case |
ports[].matrixSet |
Template | WebMercatorQuad |
Consumers already on another grid |
quality.blocking |
Template | Validity, CRS, precision | Never — this is the floor |
slo.availability |
Template | 0.999 |
Measured consumer need |
access.principals |
Template | [] — default deny |
Each addition is deliberate |
Common Failure Modes & Fixes
A new domain’s first make apply fails on credentials.
Root cause: the template assumes a role the new domain does not yet hold. Fix: role assignment belongs in domain registration, before the template is cloned — if it happens during the golden path, the path starts with a ticket.
Time-to-first-product is a week despite the template working. Root cause: a manual step outside the repository — a firewall rule, a DNS entry, an approval. Fix: instrument timestamps at clone, first apply, first publish; the gap that dominates names the step, and it usually surprises the platform team.
Domains copy the template and then diverge immediately. Root cause: the defaults are wrong for a common case, so everyone edits the same three lines. Fix: that edit pattern is the signal — change the default rather than documenting the workaround.
CI passes but a real domain’s first publish fails on quality. Root cause: the fixture partition is synthetic and clean, so the gate is never exercised against realistic data. Fix: include a deliberately defective fixture alongside the clean one and assert the gate blocks it.
The template drifts from what the platform actually supports. Root cause: nobody runs it except new domains, which is rare. Fix: the CI job above runs it on every commit to the platform, so a platform change that breaks the path breaks a build rather than a domain.
FAQ
How opinionated should the defaults be?
Uncomfortably so. Every option offered is a decision a new domain has to research, and a template with a dozen choices at each step is a research project rather than a route. The right test is whether a domain engineer who knows their data but nothing about the platform can get to a published product without reading anything beyond the manifest’s comments. That standard forces defaults for storage CRS, tile matrix set, zoom range, quality gates and SLO — and it is met by making deviation possible and slightly effortful, so a domain with a real reason takes the escape hatch and a domain without one does not think about it.
Should the template include a sample dataset?
Include a fixture, not a sample. A fixture exists so CI can exercise the whole path end to end, and it should be small, synthetic, and paired with a deliberately defective twin so the quality gate is proven to block. A realistic sample dataset is a different thing and tends to become a maintenance burden — it ages, it acquires domain-specific assumptions, and new domains start editing around it rather than replacing it. The fixture should be obviously not-real, so nobody mistakes it for a starting point.
What happens when the platform’s manifest schema changes?
The same discipline the platform asks of spatial products applies to the platform’s own interface: a version bump, a window during which both shapes are accepted, and a migration note. The template is updated in the same change, and because CI runs the template on every platform commit, a schema change that breaks it fails immediately rather than reaching a domain. Domains already provisioned continue on the old schema until they update, which is what makes the change non-breaking in practice rather than only in intent.
How do we know the golden path is still the fastest route?
Measure elapsed time, not adoption. Adoption can be high because the path is mandatory; time-to-first-published-product cannot be faked. Record timestamps at clone, first successful apply, and first successful publish, and watch the distribution across new domains. A path that took a day six months ago and takes three now has accumulated a manual step that the platform team has stopped seeing because they know the workaround.
Related
- Self-Serve Platform Capabilities for Spatial Teams — the parent topic and the capability set
- Provisioning Spatial Infrastructure with Terraform — what
make applyruns underneath - Publishing a Spatial Product from One Manifest — what
make publishdoes