Monowai

writing - service design boundaries

Services built to be consumed.

Most integration cost is self-inflicted. Where you draw a service boundary decides who pays for complexity. Draw it once, in the language of the business, and every consumer starts a step ahead - a new capability begins as configuration, not a fresh integration project.

Mike Holdsworth · Singapore

talk This argument as a deck - view the slides →

This is an abstract of a talk given inside a regulated-industry engagement. The client and its systems are generalised here; the pattern is the point, and it travels.

The cost is structural

A capable computation engine exposes its internal fields directly, and a thin request/response proxy sits over the top. Every consumer then binds to that implementation: it re-implements the same calls, re-derives the same domain logic, guesses at the terms, and waits for upstream to deploy before it can integrate. No team can build ahead.

The result is an onboarding cost that scales with capabilities × channels × consumers - the same work, re-done and re-guessed each time. That's not a consumer failing to try hard enough. It's a leaky abstraction, a distributed integration, and sequential coupling - and all three are platform concerns, not consumer ones.

None of it is essential complexity. The domain didn't demand it; the default did. This is the cost of not designing - accidental complexity, compounding release after release, because this architecture was never chosen. It's what you get when no one does.

"Let the API handle it"

You hear it in every design meeting, and it always sounds like a decision. It rarely is. "Let the API handle it" only means something if the API's boundary actually owns the thing being handled - and when the boundary is drawn around an implementation, it doesn't. The complexity hasn't been handled; it's been relocated to whoever calls next, and struck off the roadmap in the same breath.

Complexity never disappears. It either lives inside the service - resolved once, named in business language, owned by a team - or it leaks out through the interface and gets re-implemented in every consumer, where no one budgets for it because officially the API handled it. An endpoint that exposes raw fields isn't handling anything; it's forwarding the problem with a status code.

The test is what the API answers, not what it exposes. If a consumer has to understand the internals to phrase the question, the boundary is in the wrong place.

Expand the domain into a platform

The fix is to give the domain an owner and a shape. Four layers, each doing one job:

  1. 1 Core engine - The computation stays where it belongs - the existing engine keeps doing what it's good at. Nothing about this pattern asks you to rewrite it.
  2. 2 Domain service - The business's vocabulary, exposed as an API - the layer the landscape is usually missing. One canonical language, the rules that decide what's allowed, and the catalogue of what's actually available, resolved in one place instead of leaked as a field list.
  3. 3 Versioned SDK - Integration becomes a dependency you import, not a project each team repeats. Clients pin a version and configure it; the everyday questions - what's available, what's allowed, what it costs - are first-class calls. All access lives inside the library, once.
  4. 4 End-to-end verification - The SDK is proven across the services with contract tests, not assumed. A change that would break a consumer fails a pipeline - not a production incident. Consumers inherit the guarantee.

Contract-first, not guessed

The SDK is what a consumer actually holds, and it earns trust the same way contracts do: the interface is agreed up front and verified against the services, so the consumer never has to guess. Integration logic and domain knowledge live in one place, versioned, instead of being copied into every client. Teams build in parallel against the contract before the upstream service even ships.

One pattern, reused

The abstractions hold beyond the first case. The same four-layer shape serves the next domain - eligibility rules, say - with the same method: an engine, a domain service, a versioned SDK, end-to-end verified. One pattern applied twice, then reused across the business. This is what makes it a platform, not a one-off integration.

Integration should be a library you import - not a rite of passage every team repeats. Design the service once, in the language of the business, and every channel starts a step ahead.

This abstract sits alongside The platform is the product - the same conviction, from the consumer's side of the contract. Full deck available on request under NDA.

← All writing