Semantics Lost at the Boundary, and Why the Monolith Keeps Pulling
Semantics Lost at the Boundary, and Why the Monolith Keeps Pulling
Inherit a finely sliced system and, after a while, the same complaint surfaces: every service is written perfectly well, yet the whole thing composes like a brick. The instinct is to blame one badly built service, but that is rarely the root. What actually went missing is the semantics, flattened the instant it crossed a service boundary.
A boundary flattens meaning into bytes
Inside a single process, an Order is a typed thing. Its fields, its invariants, its relationship to Payment all live within reach of the compiler. Change its shape and the tooling tells you, immediately, which callers just broke. The reason you dare to refactor it, rename it, tighten a field from optional to required, is that something is watching the meaning for you.
Cross a service boundary and that world is gone. Before it leaves, Order is serialised into JSON, protobuf, some wire format; on the far side it is deserialised back. For the duration of that trip it is a blob of bytes carrying no meaning of its own. How the receiving service reads that blob rests on an implicit contract: an OpenAPI spec that may or may not be current, a paragraph of README, something an engineer said in Slack three years ago. The type system has no purchase on this line. It cannot see across, and the far side cannot see back.
So everything the tooling used to carry for free falls back onto people. Rename a field and the compiler stays silent; the downstream only blows up at runtime, on some particular record. Want to know whether a field still has callers? There is no IDE find-usages across the wire, only grepping logs, asking around, taking a bet. Migrating a data structure stops being a confident refactor and becomes a run of cross-team coordination plus careful patching in production. The semantics did not vanish. It merely moved from where tooling could verify it to where only human memory can chase it.
The monolith’s pull is not laziness
Once you see this, the recurring urge to fold back into a monolith stops being mysterious. It is not technical laziness, nor a failure to understand distributed systems. What a monolith actually gives you is this: the semantics stays alive inside one type system and one toolchain, end to end.
In a monolith, from the HTTP handler all the way down to the data layer, Order is still that Order. Change its shape and the compiler walks every caller with you. Pull a field and find-usages tells you who still touches it. Move a chunk of logic and the IDE refactors it in one stroke. These are not small comforts; they are the reason a large system can keep evolving at all. Systems ossify not because they are big, but because you gradually stop daring to touch them. A monolith keeps the cost of daring low, because the tooling stays on your side.
It also explains why some teams come out of a microservices split moving slower, not faster. They thought they were buying independent deployment and team autonomy. What they actually spent was that free layer of semantic verification. Every service boundary is one more place the tooling stops vouching for you. The more boundaries you open, the larger the surface that drops back to manual coordination. A well-factored monolith is often far easier to maintain than a set of microservices with carelessly drawn lines, and the difference is simply whether the meaning is still within tooling’s range.
Ask whether the cut is worth it
None of this is an argument against splitting services. Some boundaries should exist, for hard reasons: teams that need to deploy independently without blocking each other, a workload whose scaling profile is nothing like the rest, a blast radius that has to be isolated so one failure cannot drag the whole site down. Good reasons, all of them. When they hold, split.
The point is the order of operations. Before you cut, ask honestly: how much semantics am I trading for this boundary? If a boundary slices apart two things that are tightly coupled and change together every week, you are pouring the work the compiler used to carry straight back into cross-team coordination, and that bill rarely pays off.
Even when you do split, the semantics is not a flat write-off. Part of it can be carried back across with a shared model: a schema shared across the boundary, an IDL that generates types in several languages, and contract tests, so that the moment a provider breaks the shape, CI cries out on the consumer’s behalf instead of waiting for some record in production to find out. None of this makes crossing a service as safe as staying in-process, but it regrows a little tooling support on the boundary itself, hauling the meaning back from the purely manual abyss. The difference is whether you are patching that layer deliberately, or pretending the boundary is free.
So where should a boundary go? Where the loss of semantics is smallest, not where the org chart looks tidiest. Cut along the seam where coupling is weakest and the contract most stable, and there is less meaning to exchange across the line, so less for the tooling to drop. Draw it the other way, purely to mirror team structure or flatter an architecture diagram, and force apart two things that are semantically inseparable, and what you bought is not modularity. It is a distributed system that spends the rest of its life patching holes.
Takeaways
- When a large system resists composition, the root is usually meaning flattened into bytes and implicit contracts at the boundary, not a bad component.
- Past a service boundary the type system stops working; validation, refactoring, and migration all fall back to manual, and errors are deferred to runtime.
- The monolith’s pull is not laziness but that the semantics stays in one toolchain, where tools can still verify and refactor for you.
- Before splitting, ask how much semantics a boundary costs; a shared schema and contract tests carry part of it back.
- Put boundaries where the loss of meaning is smallest, along the weakest coupling seam, not where the org chart looks neat.
Sheng’s take, drafted with Claude · part of the 2026-06-13 blog renovation, paint still drying.