#Two files that must agree
Gateway configuration duplicates information the service already has. Routes are in the handler registrations. Request and response shapes are in the types. Protocol and port are in the service's own configuration.
Then somebody writes all of it down again in a gateway config, in a different repository, owned by a different team, deployed on a different cadence. The two agree on the day they are written.
The failure is mundane and constant. An endpoint ships, the gateway does not know, a ticket is opened, somebody edits YAML, and there is a deploy whose entire content is catching up to last week's deploy.
#Inverting who declares
The manifest protocol puts the declaration on the service. It publishes its own API surface across several schema formats, along with health information and capability hints such as whether an endpoint is safe to retry.
The manifest is generated from the same types that serve the requests, so there is no second declaration to drift. That is the module principle applied across a process boundary : the service is the only component that reliably knows its own surface.
Publishing goes through whatever discovery is already running. The protocol itself does no routing, rate limiting or traffic management, and being strict about that boundary is why two very different gateways can consume it.
#Two consumers, one protocol
One gateway is a standalone binary in Rust, suited to being a separate network element with its own scaling and blast radius. The other mounts inside a Go process as a framework extension, suited to an internal aggregation layer. Same protocol, same derived routes.
One protocol with two independent consumers is the test of whether you wrote a protocol or one program's configuration file with ambitions.
Several under-specified areas were found only because the second implementation made a different reasonable choice. Precedence for a contested path and the semantics of a manifest that fails validation were both discovered that way.
#What derivation makes possible
- Aggregated API documentation. Every service's specification is already in its manifest, so a merged document is a by-product rather than something anyone maintains.
- Routes that disappear correctly. An instance going away removes its routes. Removing a route from a file requires somebody to remember.
- Retry policy at the right altitude. The service knows whether an endpoint is idempotent. The gateway does not, and has historically been told by a human who was guessing .
#The cost
Discovery is now on the critical path for routing, and discovery is a distributed system with its own failures. If it is unhealthy, routes go stale, and a stale route is worse than a stale file because it is not visible in a diff.
Both gateways cache the last known good manifest set and keep serving from it, converting an outage into staleness. That is the right trade and it is not free, since staleness has its own bad day when an instance is genuinely gone and traffic keeps arriving until health checks catch up .
There is also a bootstrap problem I did not anticipate. The gateway needs a static route to reach discovery in the first place. There is always one piece of configuration you cannot discover, which is the same observation the reconciliation-based orchestrators arrive at .
References
- [1]D. L. Parnas, “On the Criteria To Be Used in Decomposing Systems into Modules”, Communications of the ACM, vol. 15, no. 12, pp. 1053-1058, 1972doi:10.1145/361598.361623 ↗
- [2]Brendan Burns, Brian Grant, David Oppenheimer, Eric Brewer, John Wilkes, “Borg, Omega, and Kubernetes”, ACM Queue, vol. 14, no. 1, 2016doi:10.1145/2898442.2898444 ↗
- [3]Michael T. Nygard, “Release It! Design and Deploy Production-Ready Software”, Pragmatic Bookshelf, 2nd edition, 2018
- [4]Sam Newman, “Building Microservices: Designing Fine-Grained Systems”, O'Reilly Media, 2nd edition, 2021