Distribution is Not a Strategy for Clarity

System under pressure

Introduction

Structured Clarity: A Series on Architectural Integrity

What changes first in many long-lived systems is not performance or reliability, but who gets to decide. Work that once felt like a local technical judgment starts to require alignment, and it stops being evaluated on its merits. Instead, it becomes a coordination problem.

This shows up differently depending on team structure, but the underlying dynamic is the same. In multi-team systems, touching code owned by another group introduces friction immediately, often less because of technical risk than because it blurs ownership boundaries or complicates plans that are already in motion (usually both). In single-team systems, the same pressure still applies. Any work that alters scope or sequencing becomes subject to approval before it can even be explored.

Over time, permission starts to come before understanding. Engineers learn which areas are safe to touch and which ones require convincing. Less senior developers step back from cross-cutting changes entirely, relying on a small set of people who “know how things really work.” The system still ships, and coordination still happens, but the cost of making even routine changes quietly increases.

Given that pressure, it’s not surprising where many teams land. When work feels blocked by blurred responsibility and constant negotiation, pulling things apart looks like relief. Separate codebases promise clearer ownership. Independent deploys promise freedom from other people’s timelines. Physical separation creates hard edges around code. It limits who can change what directly, and allows teams to move infrastructure, deployment, and versioning decisions independently. For groups struggling to protect focus and accountability, those benefits are real, and they arrive early enough to feel decisive.

The problem is that separation changes topology faster than it changes understanding. Coordination costs don’t disappear; they shift. Instead of negotiating changes inside a shared codebase, teams negotiate changes across service boundaries. Instead of touching code directly, they request changes from one another. The same questions about impact, timing, and responsibility resurface, just routed through APIs and queues instead of pull requests.

That’s when the mismatch becomes visible. If someone else still has to modify “their” system for you to move forward, then the boundary was never really doing the work you needed it to do. Distribution didn’t create clarity; it assumed clarity would emerge from separation. And when it doesn’t, teams are left carrying the same pressures as before, now layered on top of the permanent costs of a distributed system.


The Trap of Premature Distribution

When Design Decisions Become Negotiation Problems

Coordination pain tends to show up quietly, and earlier than most teams expect. It often begins with something simple: needing help to understand or modify code that sits just outside your immediate area of responsibility. In practice, I’ve seen this show up initially around shared code, long before services or deployment boundaries are even part of the conversation, because structure stops making responsibility clear.

When local decisions require global alignment, the system stops being evaluated on its merits and becomes a coordination problem.

At first, teams treat this as a local inconvenience. The code is messy. The boundaries are muddied. A quick explanation or a handoff will get things moving again. Those explanations are usually true, but incomplete. What’s being felt is not just friction in the code, but friction in how work is divided and coordinated. Understanding no longer travels cleanly with the change.

This is why coordination pain is so often misread. From the outside, the system still looks healthy. Features ship. Reliability holds. A disciplined team can operate this way for a long time. The cost shows up elsewhere: in longer conversations, slower decisions, and growing reluctance to work across seams that feel unpredictable or expensive to cross.

Seen this way, coordination pain isn’t the problem itself. It’s a signal that the system’s divisions are no longer serving as effective boundaries. Work that should be parallel becomes serialized. Changes that should be local start to ripple outward. Over time, people adapt by narrowing their scope of responsibility, avoiding shared areas altogether, or duplicating logic rather than negotiating access. The system still moves forward, but at the cost of clarity and momentum.

That’s why this signal matters: it appears long before anything breaks. It shows up before outages, before scaling failures, and before deadlines are visibly missed. By the time those symptoms arrive, the underlying issue has usually been present for quite a while, quietly shaping how people work.

Boundaries Without Clarity Limit Real Independence

When coordination starts to hurt, teams don’t usually frame the problem in architectural terms. They experience it as a loss of control. Decisions that once felt local now require explanation. Changes invite questions, objections, or unexpected consequences. Over time, shared code starts to feel less like a collaboration point and more like a liability.

Autonomy without shared contracts doesn’t create speed; it creates guardedness.

This is where the desire for autonomy takes hold. Engineers want to be accountable for the outcomes they influence, and insulated from the ones they don’t. When responsibility is blurred, surprises become costly: timelines shift, commitments are missed, and it’s hard to tell who owns the result. Narrowing the surface area of responsibility becomes a rational response, even when it means avoiding or duplicating work that already exists elsewhere in the system.

Once autonomy becomes the goal, behavior follows. Teams optimize for delivery they believe they can control. Isolated code paths feel safer than shared ones. Copying logic feels preferable to negotiating changes. The cost of fragmentation is distant and abstract; the cost of coordination is immediate and personal. Over time, this reinforces the belief that separation is the only reliable way to move forward.

This is why services often become the language teams reach for. A separate repository creates a hard boundary where softer ones failed. I’ve seen teams become deliberately protective of these boundaries, not out of ego, but because control was the only way to make responsibility feel survivable. Permissions can be enforced. Changes can be gated. Responsibility becomes tangible, not because the system is better understood, but because access and control are easier to define. From the inside, that feels like progress.

At that point, the conversation quietly shifts. It stops being about improving the system and starts being about who controls what. Protectiveness around code isn’t a character flaw; it’s a survival strategy in a system where boundaries don’t reliably do their job. Teams aren’t asking for services. They’re asking for clear ownership.

How Hesitation Becomes a Permanent Development Tax

When teams separate systems, some things do genuinely change. Execution environments become isolated. Deployments decouple. Repositories provide clearer control over who can make changes and how those changes are reviewed. For a time, delivery friction drops. Fewer people are involved in day-to-day decisions, and work moves faster simply because there are fewer shared surfaces to negotiate.

Complexity doesn’t arrive with a loud failure; it surfaces as a quiet, steady increase in the effort required to make even the simplest changes.

It’s reasonable to expect more than that. Teams often assume that coordination itself will become easier, that separation will eliminate the need for constant alignment. If the problem feels like too many dependencies and too many conversations, pulling those dependencies apart looks like a direct fix. In practice, though, the underlying relationships remain. Anything that interacts must still change together, be versioned together, or be reasoned about together. Essential coupling doesn’t disappear just because code moves into a new process.

What changes is the shape of coordination. Instead of direct collaboration inside a shared codebase, work flows through queues and backlogs. Requests replace edits. Conversations become tickets. Dependencies are managed through formal pipelines rather than informal ones. At first, this feels cleaner and more controlled. But as soon as progress depends on someone else making a change, the familiar friction returns, often with more latency and less flexibility than before.

The mismatch is easy to miss because the early benefits are real. Control arrives quickly. Teams feel insulated from unrelated changes. The costs arrive later. Distributed execution expands the operational surface area, complicates observability, and introduces failure modes that aren’t immediately visible. By the time those costs are felt, the system has usually absorbed significant sunk cost, both technical and organizational.

The problem wasn’t that things were separated. It was that separation was treated as a substitute for clear structure. Distribution changed where code ran and who could touch it, but it didn’t clarify responsibilities or reduce the coordination required to make meaningful changes. Without that clarity, the same pressures reassert themselves, just routed through a different topology.


The Mirage of Distribution

Confusing Physical Topology with Logical Boundaries

A logical boundary (what DDD would call a bounded context) is a separation of responsibility in the code itself. It’s not primarily about who owns a repository or which team maintains a service. It’s about clarity of purpose. When a boundary is well formed, it’s easy to explain what a piece of code does, why it exists, and what kinds of changes belong there. Cohesion is high. Coupling is constrained. You don’t need to read the rest of the system to reason about a local change.

Separation changes topology faster than it changes understanding.

Physical separation solves a different problem. Running code in a separate process or repository creates clear execution boundaries. It enables independent deployment. It limits blast radius. It reduces the chance that one group’s operational issues spill directly into another’s. These are real benefits, and for many teams they’re the first ones that matter. Schedule control alone is often enough to justify the move.

Where teams get into trouble is assuming these two kinds of boundaries are interchangeable. Physical separation feels like freedom, so it’s tempting to treat it as a substitute for structural clarity. If the goal is autonomy, then isolating execution looks like the fastest path to get there. The expectation is that clear responsibility and clean coordination will follow naturally.

They don’t. When physical separation outruns logical clarity, the result is usually a system that is distributed but not well structured. Dependencies remain dense. APIs become tightly coupled. The same questions about where changes belong and who should make them resurface, now scattered across processes instead of files.

This distinction matters because it’s hard to undo later. Once execution boundaries are in place, structural mistakes get locked in. Changing where code runs is expensive; changing what code means is even harder. Splitting along the wrong lines doesn’t eliminate coordination, it just pushes it into stranger and more rigid shapes. At that point, teams are left paying the costs of distribution without getting the clarity they were after.

Logical boundaries answer the question of what a piece of code is responsible for. Physical separation answers the question of how that responsibility is carried out in practice. Confusing the two is how teams end up with systems that look modular from the outside, but still require constant negotiation to change.

Just because something runs separately doesn’t mean it’s properly modular.

The Three Degrees of Micro-ness

When teams talk about “microservices,” they’re rarely talking about a single, well-defined thing. More often, they’re expressing a desire for autonomy. They want control over their schedules, their changes, and their delivery cadence. They want to stop coordinating every meaningful decision. “Microservices” becomes shorthand for independence.

Micro-ness is not a single binary choice; it is a ladder of complexity spanning modules, repositories, and execution boundaries.

The problem is that independence doesn’t come from one architectural move. What gets bundled under the microservices label is actually a set of separate decisions that can be made independently. Treating them as one obscures the tradeoffs and makes it harder to reason about sequencing.

One axis is modularity: how finely responsibilities are separated within the code. This is about cohesion, coupling, and whether changes can be understood and made locally. It determines whether boundaries are real in practice, regardless of how code is packaged or deployed.

A second axis is repository and deployment independence: whether parts of the system can be built, tested, and released independently. This is where schedule control comes from. It’s also where teams often assume they need new repositories or services, even though those outcomes can often be achieved without changing execution topology.

The third axis is execution isolation: whether code runs in separate processes, with separate failure domains and operational characteristics. This is the most expensive axis to move along. It changes observability, failure modes, and runtime responsibility. It also tends to arrive early in conversations, because it’s the most visible and the easiest to point to.

These axes are related, but they are not dependent. Teams can split deployment without splitting teams. They can split teams without splitting execution. They can even split execution without improving structure at all. The common failure mode is moving along multiple axes at once, assuming they rise together, when in reality they solve different problems.

Micro-ness isn’t a destination. It’s a set of dials. Turning the wrong ones first is how teams end up paying costs they didn’t need to incur.

In practice, many teams don’t end up with fleets of small, independently evolving services. What emerges instead are larger units of responsibility that present clear external boundaries, but change internally as a coherent whole.

These systems aren’t small, but they’re stable.

The Fallacy of Separation-as-Clarity

The trouble starts when separation happens faster than structure. The earliest signs aren’t dramatic, but they appear quickly once code is pushed across a boundary that doesn’t really fit. Calls that were simple and local become brittle once they’re out of process. Interfaces harden before responsibilities are clear. What used to be an internal refactor turns into a coordination exercise almost immediately. 

If you can’t draw the boundary cleanly while everything is still in one place, distributing it only raises the cost of fixing it later.

And to be fair, this isn’t always naïveté. Often the team knows the structure isn’t ready, but timeline, scale, or organizational constraints force topology changes anyway.

This is where distributed monoliths come from. Shared databases linger because untangling them is harder than splitting the service itself. APIs grow dense, encoding the same implicit contracts that used to exist in code. Background processes and integration points multiply, not because they reflect clear domain boundaries, but because execution boundaries were introduced before the structure underneath them was ready. What was meant to create autonomy ends up recreating the same coupling in a more rigid form.

I’ve watched teams extract a service early because it appeared independent, only to realize later that its behavior still depended heavily on surrounding code. Once it lived in its own repository, reversing course wasn’t practical, and finishing the work required re-implementing logic that had previously been shared. The result wasn’t failure, but a delivery that took far longer than anyone expected.

Once execution boundaries are in place, these mistakes are hard to undo. Reintegrating separated code is expensive, both technically and organizationally. Over time, the running costs of the decision outweigh the cost that would have been required to rethink the structure earlier. The system absorbs complexity it never needed to carry, not because separation was wrong, but because it arrived before clarity did.


Macro-services and the Modular Core

The Macro-service as the Strategic Destination

If premature distribution fails because execution boundaries harden before responsibility does, the problem isn’t distribution itself, but choosing the wrong unit of structure too early.

The goal is a system where responsibility is enclosed and change remains local.

The question isn’t how small a service should be. It’s whether the unit of responsibility holds up over time. Teams struggle with ownership not because systems are large, but because responsibility is fragmented in ways that force constant coordination. Even very large systems can be workable when they’re modular. Conversely, very small services can become fragile if they duplicate effort or require synchronized change.

This is where the idea of a macro-service becomes useful. A macro-service is not a single, indivisible thing. It’s a cohesive unit that may contain multiple internal services or components, but presents a stable surface of responsibility to the rest of the system. It occupies the middle ground between “everything is shared” and “everything is isolated.” It’s big enough to contain meaningful behavior, and small enough to reason about as a whole.

Stability matters more than size. One of the clearest signals of whether a boundary is well chosen is how often it has to be renegotiated. Data schema changes are a good example. When every schema adjustment forces coordination across multiple groups, the boundary is likely cutting across responsibility instead of enclosing it. When boundaries are right, schema evolution mostly happens within a single logical context, and changes propagate outward intentionally rather than accidentally.

At the right scale, ownership starts to work the way people hope it will. Most changes can be made without asking for permission. Coordination shifts from negotiation to cooperation. Teams still respond to external needs, but they do so from a position of control rather than fragility. The blast radius of change is clearer, and the cognitive load required to make decisions stays local.

This applies regardless of team structure. Before teams are split, clear modular boundaries reduce cognitive overhead and make the system easier to evolve. After teams exist, assigning responsibility for one or more macro-services gives those teams real ownership without forcing premature decisions about execution or deployment. Internal splits can happen when they’re justified by scale or operational concerns, not because they’re needed to establish basic autonomy.

A macro-service is small enough to be coherent and cohesive, but large enough to encompass a clear division of responsibility and behavior within the system. That combination is what makes ownership durable. Without clear boundaries, ownership quickly degrades into gatekeeping or avoidance. With them, it becomes a tool for sustained clarity rather than short-term control.

Ownership only works when boundaries are clear.

The Modular Monolith as the Permanent Structural Core

A monolith is modular when separation is a choice, not a prerequisite. In a modular monolith, the core domain logic is organized into coherent units that can be reasoned about independently. If a piece of functionality needed to move into its own service, it could do so without first reworking the internal structure of the code. The only changes required would be at the boundaries where in-process calls become over-the-wire communication. The logic itself wouldn’t care. I’ve seen services extracted this way by adding a new runtime and reusing existing domain logic, because execution concerns were already isolated.

A modular monolith is not a stepping stone; it is the permanent structural core that preserves locality of reasoning.

That distinction matters. In a modular monolith, execution details are secondary to understanding. You don’t need to cross repository boundaries to trace behavior. The code that defines what the system does lives together, while the code that determines how it runs can live wherever it makes sense. Those concerns are related, but they’re not fused.

This is why “backbone” is the right metaphor. A modular monolith supports change without demanding commitment. You can introduce a separate execution context for a hot path without reshaping the domain. You can pull functionality out into a service when scaling or isolation demands it. In a well-modularized system, separating and reintegrating behavior is a controlled operation, not a rewrite.

That reversibility is the quiet superpower here. When code is split across repositories and services before the structure underneath is ready, those decisions become expensive to undo. A modular monolith keeps the cost of change low by keeping the structure intact even as execution choices evolve.

Most of the benefits teams associate with distribution arrive earlier than they expect once boundaries are clear. Ownership becomes meaningful. Changes can be made in isolation. Coordination drops because responsibility is obvious, not because execution is isolated. With a solid CI/CD pipeline, deployment cadence becomes a non-issue for most changes. Whether something ships in an hour or at the end of the day matters far less than whether it was safe and local to begin with.

A modular monolith is not an end state. It’s the structural core that keeps future decisions cheap. When boundaries are clear, execution can change without forcing the system to reorganize itself first. Distribution becomes something you can do deliberately, and undo when needed, instead of a one-way door you have to justify forever.

The Hard Limit of Data Entanglement

Boundaries aren’t tested by diagrams or design reviews. They’re tested by change. The fastest way to find weak boundaries is to watch what breaks when the system evolves. Schema changes are often the first signal. If adding a feature requires modifying data or logic in multiple unrelated places, the boundary is likely cutting across responsibility instead of enclosing it. The same is true when parts of the system need to move at different speeds. If accelerating one area consistently drags others along with it, something upstream isn’t well formed.

If you separate the code but leave the data entangled, you haven’t distributed the system; you’ve just made the coupling harder to see.

Data makes these problems hard to ignore. Data is inherently contextual, and it resists being shared without consequence. When one part of the system reaches directly into another part’s data, coordination is no longer optional. If shipping logic depends on payment tables, or unrelated areas write to the same schema, the boundary may exist in code but not in reality. By contrast, sharing data within a coherent context is expected. That shared understanding is often what defines the boundary in the first place.

Over time, change patterns tell a clear story. Areas that always change together are usually part of the same responsibility, whether the system acknowledges that or not. If boundaries are right, changes within one context shouldn’t routinely force changes in another. Some concerns, like permissions or shared policy, are inherently cross-cutting and will require coordination even in well-structured systems. But when local evolution fails to stay local, teams end up negotiating work that feels surprising: touching code they don’t own, coordinating changes that don’t align with their intent, or revisiting decisions they thought were already settled.

These surprises are not accidents. They’re signals. They indicate that separation happened without sufficient structural clarity, or that boundaries were drawn along convenient lines instead of meaningful ones. Simply separating code isn’t enough. Without clear ownership of data and responsibility, the same coordination problems resurface, regardless of how many services exist.

A boundary that survives contact with reality is one that is clear and contextually separate. It encloses its data, absorbs most of its own change, and exposes dependencies intentionally rather than accidentally. That clarity makes it possible to reason about change without constantly renegotiating responsibility.

Good boundaries aren’t designed once. They’re clear enough to be re-evaluated at any time with minimal cost.


Conclusion

Software rarely becomes hard all at once. More often, it becomes hard quietly, as clarity erodes and coordination fills the gap. Teams respond to that pressure in reasonable ways. They look for independence. They look for control. Distribution enters the conversation not as a goal, but as an attempted release valve.

The problem isn’t that those instincts are wrong. It’s that they’re often aimed at the wrong lever. Separating execution changes how systems are deployed and operated, but it doesn’t determine whether work can be understood, owned, or changed safely. Those outcomes are shaped earlier, by structure itself.

Microservices bundle several independent decisions, and many teams move along those axes together without realizing it. Execution isolation arrives before responsibilities are clear. Repositories split before boundaries stabilize. The result isn’t autonomy, but a system that is harder to change and harder to undo. Distribution amplifies whatever structure already exists. It doesn’t correct it.

In practice, teams don’t need smaller systems so much as they need coherent ones. Macro-services work when they enclose clear responsibility, own their data, and allow most change to remain local. For many systems, the most effective way to achieve that is not through pervasive distribution, but through a modular monolith that serves as the permanent structural core.

A modular monolith isn’t something teams grow out of. It’s the form that lets a macro-service remain understandable and evolvable over time. Distribution can be layered on selectively when isolation, scale, or operational concerns demand it, but it doesn’t need to be the default, and it doesn’t need to be the destination. That demand usually shows up as a hot path that needs independent scaling, a failure domain that must be isolated, or a regulatory boundary that can’t be crossed.

Most systems don’t need to be smaller. They need to be clearer.


All content is based on my own research, learnings and real-world implementation experience. Visuals are custom 3D assets built and rendered in Blender. Read more about my craft here.