Field Note 47current
Govern the contract between teams, not the code inside them
TL;DR
Draw the org so each team owns its services end to end, and put one team in front as the gateway that dispatches to the rest. Then govern exactly one thing centrally: the contract at the boundary — the schema, the identity model, the error and idempotency semantics, the quotas, the versioning and deprecation rules. That contract is the API of your organisation.
Do not mandate the libraries, frameworks, or processes a team uses to satisfy it. Ship a reference implementation — an SDK, a service template, a “paved road” — as an opt-in blueprint, not a fence. A team may adopt it wholesale, fork it, or reimplement it in another language; you don’t care, because the gateway enforces the contract at runtime, so you never need to enforce the code behind it. Verify behaviour at the boundary, not the provenance of what produced it.
That single move — central contract, sovereign internals — is what lets every team refactor, rewrite (ZFN-23Field Note · currentZFN-23 — Rewriting an implementation is fine — refactoring isn't always the answerRefactoring isn't always right. When the structure is wrong at the root, it's fine — often better — to rewrite an implementation from scratch. Clean interfaces and data models make the implementation disposable: stable contract, swappable internals. LLMs make it cheaper still.Open ZFN-23 →), re-language, and ship on its own cadence without asking anyone’s permission. The interface is the unit of governance; everything else is local.
Context
This is Conway’s law (How Do Committees Invent?Melvin E. Conway — How Do Committees Invent? (1968)The original paper behind 'Conway's law': any organisation that designs a system will produce a design whose structure is a copy of the organisation's communication structure. The team boundaries you draw become the interfaces in your system whether you intend them to or not.melconway.com ↗) used on purpose instead of suffered by accident. The boundary between two teams becomes an interface in your system whether you design it or not — so design it, and put your governance there. The architecture you want falls out of the org you draw.
The common mistake is governing the wrong layer. A platform team mandates “everyone uses our HTTP client, our logging library, our framework, our deploy pipeline,” believing it’s manufacturing consistency. What it’s actually manufacturing is synchronous coupling dressed up as a standard. Every team now upgrades on the platform team’s cadence; a bug or CVE in the shared library freezes the whole org until one team cuts a patch and everyone redeploys in lockstep; a team that wants a different language is locked out; and the platform team becomes a bottleneck sitting in everyone’s critical path. You set out to build independent teams and you built a distributed monolith with extra steps and worse latency.
Meanwhile the thing that actually matters — the contract at the boundary — is too often left vague, “specified” by reading the other team’s source and guessing. So you end up with the worst of both worlds: rigid, mandated implementations and loose, undocumented interfaces. It should be the exact opposite. Pin the interface hard; leave the implementation free.
A gateway makes this tractable because it’s a real runtime chokepoint — the one place every request already passes through. It’s the natural home for the things that genuinely must be uniform (authn, identity propagation, quotas per ZFN-18Field Note · currentZFN-18 — Enforce a quota at ingress on every endpoint — even unabused onesPut a quota on every endpoint and enforce it at ingress from day one — per tenant, principal, IP — even for endpoints nobody abuses yet. Unlimited-by-default means the first runaway client or compromised key is an outage. Return 429 + Retry-After; retrofitting limits is painful.Open ZFN-18 →, schema validation, telemetry conventions) precisely because you can enforce them there instead of trusting that everyone imported the right helper. It’s ZFN-34Field Note · currentZFN-34 — A resource-free 'bouncer' account: the single gateway to customer resourcesFunnel access to customer resources through one dedicated account that holds no resources. Customer trust names only its role; the role is denied from your own org (aws:ResourceOrgID). Fenced both ways, it shrinks the confused-deputy surface to one audited gateway.Open ZFN-34 →’s “single audited gateway” idea pointed inward, at your own teams.
Recommendation
Centralise the contract; decentralise everything behind it. Make the gateway enforce the contract, and make the blueprint optional.
-
Teams own their services end to end — design, build, run, on-call (ZFN-31Field Note · currentZFN-31 — Own your components — when you deeply understand the domainOwning your own components rather than generic off-the-shelf services is often the better path as you grow: own what's core, lean on small vetted libraries for the hard parts. LLMs make it attainable at smaller scale — but only when you truly understand the domain, or it hurts.Open ZFN-31 →). Ownership includes the freedom to choose how, which is the half people forget.
-
One team owns the gateway, and its product is the contract and the dispatch fabric — not the services. This is a platform-owned capability in the sense of ZFN-5Field Note · currentZFN-5 — Make workload identity a platform-owned serviceWorkload identity belongs in shared platform infrastructure, not reimplemented per service. A small token service mints short-lived tokens any service verifies. Shared keys are a fine first step; asymmetric signing the better end-state — don't let 'no PKI' block it.Open ZFN-5 →: a thing the org consumes self-service, run as a product with versions, docs, and a support story. The gateway team’s customers are the other teams.
-
Write the contract down precisely, because vagueness re-couples everyone. Schema-first, with generated clients (ZFN-14Field Note · currentZFN-14 — Define every API with a schema, and generate the clientsDefine every API with a machine-readable schema (OpenAPI, Protobuf, GraphQL) as the source of truth, and generate clients and server stubs from it — never hand-roll request-building and JSON parsing. Hand-written clients drift and break silently; check schema compatibility in CI.Open ZFN-14 →); standard protocols rather than bespoke ones (ZFN-30Field Note · currentZFN-30 — Use the standard; don't reinvent the protocolWhen a standard exists for a common or complex problem, use it — don't reinvent the protocol. Standards encode huge adversarial expertise, especially in auth and crypto; a partial implementation beats rolling your own. You're not that special, and your problem isn't either.Open ZFN-30 →); explicit error semantics and idempotency, with read-only and idempotent operations annotated (ZFN-19Field Note · currentZFN-19 — Annotate read-only and idempotent endpoints; make every mutation idempotentAnnotate every endpoint as read-only (safe) or idempotent, in the schema, so infrastructure can retry, route to replicas, and cache safely. Make every state-changing endpoint idempotent (idempotency keys for create/charge/send); a non-idempotent retry double-applies.Open ZFN-19 →); identity propagation rules; quotas and flow-control behaviour (ZFN-13Field Note · currentZFN-13 — Fail fast and push back: retries, load shedding, and flow controlBuild client retries (backoff, jitter, Retry-After) from day one. Under overload, shed fast and push the failure back to the source to retry — don't retry internally and amplify it. Flow-control everywhere, bound every queue, and don't take more work than you can finish in time.Open ZFN-13 →, ZFN-18Field Note · currentZFN-18 — Enforce a quota at ingress on every endpoint — even unabused onesPut a quota on every endpoint and enforce it at ingress from day one — per tenant, principal, IP — even for endpoints nobody abuses yet. Unlimited-by-default means the first runaway client or compromised key is an outage. Return 429 + Retry-After; retrofitting limits is painful.Open ZFN-18 →). If it isn’t in the contract, it isn’t guaranteed — and if a team relies on something not in the contract, that’s the bug.
-
Enforce the contract at the gateway, at runtime. This is the keystone. You check that an endpoint honours the contract — validates against the schema, returns the agreed errors, behaves idempotently — not that it was built with your code. Idempotency is a property of the endpoint, not of which library produced it. Once enforcement lives at the boundary, mandating the implementation becomes not just unnecessary but actively counterproductive. This is the modern echo of the Amazon API mandateThe Amazon API mandate (recounted in Steve Yegge's 'Platforms Rant', 2011)Amazon's ~2002 directive: all teams expose data and functionality through service interfaces only, with no back-door access; teams may build whatever they like behind the interface. The interface is the contract; everything behind it is the team's own business.gist.github.com ↗: interface-only, no back doors, build whatever you like behind it.
-
Provide the blueprint as a paved road, not a law. Ship a reference SDK and a service template that satisfy the contract out of the box, handle the boring correctness (retries, idempotency keys, auth, telemetry), and make the right thing the easy thing. Then let teams take it or leave it — that’s a Thinnest Viable PlatformTeam Topologies — Skelton & Pais (2019)Defines four team types and three interaction modes, and argues a platform should be run as an internal product — a 'Thinnest Viable Platform' that stream-aligned teams consume self-service to reduce cognitive load. The platform earns adoption by being good, not by mandate.teamtopologies.com ↗ that earns adoption by being good. If your blueprint is genuinely better than what a team would write, most teams will use it, and the few who don’t usually have a real reason (a different language, a latency budget, a domain they understand more deeply). A blueprint is a gift; a mandate is a leash.
-
Keep the gateway thin — defend this with your life. It routes, authenticates, propagates identity, meters, validates schema, sheds load, and observes. It does not hold business logic. The moment cross-team logic starts accreting in the gateway, you’ve quietly recentralised: it becomes the scariest deploy in the building, every team is coupled to it again, and you’re back to the monolith you were trying to avoid. Boundary concerns only — keep the control-plane and data-plane split honest (ZFN-16Field Note · currentZFN-16 — Separate the data plane from the control planeSplit the serving path (data plane) from the management path (control plane). The data plane keeps serving on last-known-good config when the control plane is down — never call it on the hot path. Coupling them turns a control-plane bug into a serving outage.Open ZFN-16 →).
Identity is the sharp, concrete example of all of this. When the gateway dispatches a request downstream, it must forward the caller’s identity to the service — delegate, never impersonate (ZFN-38Field Note · currentZFN-38 — Agents are principals: delegate, never impersonateAn agent acting with a copied user credential is impersonation — untraceable by design. Give agents their own identities and keys; let them act for a human only through explicit, scoped, time-bounded, revocable delegation; and record both actor and principal on every action.Open ZFN-38 →) — and must never collapse every request into an anonymous “gateway” system actor (ZFN-40Field Note · currentZFN-40 — No anonymous "system" actorIf "system" appears as an actor in your audit log, attribution is already broken. Every automated action — cron job, cleanup task, migration, agent — runs as a named identity with its own credentials and scope, so "who did this?" has an answer and revocation is surgical.Open ZFN-40 →). That’s a contract rule, and the gateway is exactly where you enforce it. How each downstream service then consumes the propagated principal is entirely its own business. Central rule, sovereign implementation — the whole pattern in one example.
From the field
The failure mode that converted me wasn’t a gateway — it was a mandated shared library. One “blessed” client, imported everywhere, required. It was good code. Then a vulnerability landed in one of its transitive dependencies, and because every service was pinned to it, the entire estate was exposed at once and nothing could ship a fix until the platform team cut a release and every team redeployed in lockstep. A mandate had turned dozens of independent services into a single fate- sharing unit — the exact opposite of why we’d split them up. The contract those services spoke at the boundary hadn’t changed at all; we’d coupled ourselves on the implementation for no interface reason. After that I stopped mandating libraries and started mandating contracts. Same library, offered as a default instead of a law, would have let each team patch and ship on its own clock.
Consequences
Easier:
- Teams ship on their own cadence and rewrite their internals freely (ZFN-23Field Note · currentZFN-23 — Rewriting an implementation is fine — refactoring isn't always the answerRefactoring isn't always right. When the structure is wrong at the root, it's fine — often better — to rewrite an implementation from scratch. Clean interfaces and data models make the implementation disposable: stable contract, swappable internals. LLMs make it cheaper still.Open ZFN-23 →) — adopt a new language, swap a datastore, re-architect — without a cross-team migration, because nothing outside depends on how they did it.
- The gateway team scales, because it owns a contract and a thin fabric rather than sitting in the release path of every service. Its blast radius and its workload are bounded by the boundary.
- You get uniform external behaviour — auth, quotas, identity propagation, telemetry — without uniform internals. The things that must be consistent are, and the things that needn’t be aren’t.
- Onboarding has an obvious on-ramp: the paved road gets a team to a conformant service fast, and opting off it later is cheap because the contract, not the code, is what actually holds.
Harder:
- You have to actually specify the contract — rigorously, including the error and identity and idempotency semantics people like to leave implicit. A vague contract leaks implementation details and silently re-couples teams; precision is now real, ongoing work.
- You need contract and conformance testing at the boundary instead of leaning on a shared library’s correctness. The gateway-enforced checks and a conformance suite are the price of not mandating code.
- The blueprint has to be genuinely excellent and genuinely maintained, or teams route around it and you get worse fragmentation than a mandate would have. An optional paved road only works if it’s the path of least resistance.
- You accept duplication: several teams each re-implement bits of the paved road in their own stack. That redundancy is the deliberate price of decoupling — pay it on purpose, don’t let it shame you back into a mandate.
New obligations:
- Run the contract as a versioned product: documented, with a deprecation policy, sunset timelines, and a clear owner for breaking changes — recorded as decisions (ZFN-1Field Note · currentZFN-1 — Keep engineering decision recordsRecord significant engineering decisions as short, versioned markdown files — context, decision, consequences. Write one for cross-team contracts, directional principles, hard-to-reverse choices, and conventions others must follow. Cite them instead of re-arguing.Open ZFN-1 →) so the why survives. The contract is now a first-class artefact with a lifecycle, not a wiki page.
- Hold the line on a thin gateway. “Just put this one bit of logic in the gateway, it’s easier” is how the distributed monolith grows back. Every addition to the gateway is a new thing every team is coupled to; treat it as such.
- Treat the blueprint as a maintained product with its own roadmap, not a one-time scaffold you bless and abandon. A paved road that rots becomes a dirt track everyone drives around.
References
- Conway — How Do Committees Invent? (1968) — why team boundaries become system interfaces; the case for governing them deliberately.
- Team Topologies — Skelton & Pais — platform-as-product and the Thinnest Viable Platform; a blueprint earns adoption rather than compelling it.
- The Amazon API mandate — interface-only, no back doors, build whatever you like behind it; this note is that mandate plus “and here’s a paved road.”
- ZFN-5Field Note · currentZFN-5 — Make workload identity a platform-owned serviceWorkload identity belongs in shared platform infrastructure, not reimplemented per service. A small token service mints short-lived tokens any service verifies. Shared keys are a fine first step; asymmetric signing the better end-state — don't let 'no PKI' block it.Open ZFN-5 → — the gateway as a platform-owned, self-service capability.
- ZFN-34Field Note · currentZFN-34 — A resource-free 'bouncer' account: the single gateway to customer resourcesFunnel access to customer resources through one dedicated account that holds no resources. Customer trust names only its role; the role is denied from your own org (aws:ResourceOrgID). Fenced both ways, it shrinks the confused-deputy surface to one audited gateway.Open ZFN-34 → — the single-audited-gateway pattern, here pointed inward at your own teams.
- ZFN-14Field Note · currentZFN-14 — Define every API with a schema, and generate the clientsDefine every API with a machine-readable schema (OpenAPI, Protobuf, GraphQL) as the source of truth, and generate clients and server stubs from it — never hand-roll request-building and JSON parsing. Hand-written clients drift and break silently; check schema compatibility in CI.Open ZFN-14 → and ZFN-30Field Note · currentZFN-30 — Use the standard; don't reinvent the protocolWhen a standard exists for a common or complex problem, use it — don't reinvent the protocol. Standards encode huge adversarial expertise, especially in auth and crypto; a partial implementation beats rolling your own. You're not that special, and your problem isn't either.Open ZFN-30 → — the contract is a schema, and a standard one where possible.
- ZFN-18Field Note · currentZFN-18 — Enforce a quota at ingress on every endpoint — even unabused onesPut a quota on every endpoint and enforce it at ingress from day one — per tenant, principal, IP — even for endpoints nobody abuses yet. Unlimited-by-default means the first runaway client or compromised key is an outage. Return 429 + Retry-After; retrofitting limits is painful.Open ZFN-18 →, ZFN-19Field Note · currentZFN-19 — Annotate read-only and idempotent endpoints; make every mutation idempotentAnnotate every endpoint as read-only (safe) or idempotent, in the schema, so infrastructure can retry, route to replicas, and cache safely. Make every state-changing endpoint idempotent (idempotency keys for create/charge/send); a non-idempotent retry double-applies.Open ZFN-19 →, ZFN-13Field Note · currentZFN-13 — Fail fast and push back: retries, load shedding, and flow controlBuild client retries (backoff, jitter, Retry-After) from day one. Under overload, shed fast and push the failure back to the source to retry — don't retry internally and amplify it. Flow-control everywhere, bound every queue, and don't take more work than you can finish in time.Open ZFN-13 → — boundary behaviours the gateway enforces.
- ZFN-38Field Note · currentZFN-38 — Agents are principals: delegate, never impersonateAn agent acting with a copied user credential is impersonation — untraceable by design. Give agents their own identities and keys; let them act for a human only through explicit, scoped, time-bounded, revocable delegation; and record both actor and principal on every action.Open ZFN-38 → and ZFN-40Field Note · currentZFN-40 — No anonymous "system" actorIf "system" appears as an actor in your audit log, attribution is already broken. Every automated action — cron job, cleanup task, migration, agent — runs as a named identity with its own credentials and scope, so "who did this?" has an answer and revocation is surgical.Open ZFN-40 → — propagate identity across the dispatch; don’t impersonate, don’t go anonymous.
- ZFN-23Field Note · currentZFN-23 — Rewriting an implementation is fine — refactoring isn't always the answerRefactoring isn't always right. When the structure is wrong at the root, it's fine — often better — to rewrite an implementation from scratch. Clean interfaces and data models make the implementation disposable: stable contract, swappable internals. LLMs make it cheaper still.Open ZFN-23 → and ZFN-31Field Note · currentZFN-31 — Own your components — when you deeply understand the domainOwning your own components rather than generic off-the-shelf services is often the better path as you grow: own what's core, lean on small vetted libraries for the hard parts. LLMs make it attainable at smaller scale — but only when you truly understand the domain, or it hurts.Open ZFN-31 → — what independent teams get to do once only the contract is fixed: own and rewrite their internals.
- ZFN-16Field Note · currentZFN-16 — Separate the data plane from the control planeSplit the serving path (data plane) from the management path (control plane). The data plane keeps serving on last-known-good config when the control plane is down — never call it on the hot path. Coupling them turns a control-plane bug into a serving outage.Open ZFN-16 → — keep the gateway a thin control/dispatch layer, not a place business logic accretes.
Changelog
- 2026-06-29: First published as a Field Note.