Platform engineering · · By Keegan Ott · 3 min read
One .NET platform for Azure and disconnected Kubernetes
“One codebase” sounds reassuring until every service contains an
if (selfHosted). I wanted the two editions to share
product behaviour without pretending Azure and a disconnected cluster
are the same place.
Where I drew the line
Domain rules, use cases, contracts and validation should not know whether a message travels through Azure Service Bus or RabbitMQ. Infrastructure adapters implement capabilities such as messaging, object storage, search, secrets and signalling behind explicit application-owned interfaces.
I did not arrive at that boundary in one clean design session. It came from noticing which changes felt wrong: a broker detail reaching a handler, a cloud setting appearing in shared code, or a harmless product change requiring two implementations. Those irritations were better feedback than another diagram.
This is not an excuse for an interface around every SDK call. Abstract stable behaviours the product needs, not the entire surface area of a vendor service.
Swap capabilities, not business rules
Each edition selects concrete adapters at startup: Azure-managed implementations in cloud deployments and locally operated implementations on Kubernetes. Configuration should fail fast when a required capability is missing or incompatible.
- Keep message contracts and handler logic shared.
- Keep deployment manifests and infrastructure provisioning edition-specific.
- Expose capability differences explicitly rather than scattering environment checks.
- Test each adapter against the same behavioural contract suite.
if (selfHosted) branches, the edition boundary has leaked
into the product.
The adapters are not equivalent
Azure Service Bus and RabbitMQ differ; cloud search and a local search engine differ. Define the minimum semantics the application requires, then document edition-specific limits. Pretending products are identical encourages accidental reliance on behaviour one adapter cannot provide.
Keep one delivery cadence
Build shared binaries from one commit where practical, then package them into edition-specific release artefacts. Run the common test suite once and adapter integration suites in representative environments. A feature is complete only when its required capabilities exist in both editions. If they do not, I put the difference in the support matrix instead of hiding it in code.
There is pressure to call a feature shared once it compiles for both editions. I found the stronger definition was operational: both packages need to be built, exercised and supportable from the same change. Otherwise the slower edition quietly becomes a separate product maintained by whoever remembers it exists.
Observe using a common vocabulary
Metrics and traces should describe operations, messages and workflows rather than vendor resources. Export them through edition-specific plumbing. This gives operators comparable dashboards and lets engineering investigate behaviour without mentally translating two telemetry models.
Sometimes cloud-only is the honest answer
Some features may depend deeply on a managed capability with no responsible offline equivalent. Marking them cloud-only is better than shipping a weak imitation. I would rather label a feature cloud-only than pretend two very different implementations offer the same thing.
My practical test is mundane: can an ordinary product change land once, while a broker or deployment change stays inside its edition? If so, the boundary is doing useful work. If not, another grand abstraction is unlikely to rescue it.