The Engineering Tax · Article 6 of 29
Treat Every Module Like an Outside Company
The next article treats every module like an outside company, not a department you can rifle through when a deadline is tight. It lays out the Autonomous Cooperation Protocol: self-contained requests, hard ownership at the interface, and the economic case for refusing the "just in case" field that looks free today and bills forever. It also names the speed trap that starts in the C-suite and ends with engineers taking shortcuts, then shows why throwing more headcount at a tangled system is the mark of a manager who misread the bottleneck. If you've ever watched a clean boundary die so one ticket could ship this sprint, this is the discipline that would have stopped it.

Autonomous Cooperation Protocol
A modular system treats each module as an independent service provider, more like a separate company than a department inside one. The module does a specific job for the entity that called it, which we’ll call the caller, and the caller isn’t the end user of the whole product. The end user is the responsibility of whoever owns the user-facing experience, not every module down the stack. This is the discipline that keeps business logic encapsulated and stops modules from making decisions based on global state or context they shouldn’t have access to in the first place. The full set of rules this article describes has a name. It’s the Autonomous Cooperation Protocol, and the rest of the article is the protocol itself: the rules a module has to honor, the boundaries it has to defend, and the costs of breaking either.
Before going further, a quick note on the word “independent,” because it’s the source of a common misunderstanding. Every module exists because something else needs the job it performs. Modules are independent in the sense that they can be replaced, refactored, or rewritten internally without asking anyone’s permission, but only as long as they keep delivering what the rest of the system demands from them. Other modules are stakeholders, which is a useful word here because it captures the relationship precisely. A stakeholder gets a say in what the service must deliver, but the stakeholder’s involvement ends at the interface. Ownership, operations, and internal data access belong to the team that runs the service. If a service isn’t being demanded, it has no reason to exist, and that includes the user module, the account module, and the rest. Demand is the only thing that justifies a service’s existence in the first place. The same logic that justifies creating a service is the same logic that justifies keeping it alive, which is the same logic that should justify killing it the day the demand goes away.
One important clarification on the word “sovereign.” Sovereignty in a modular system means one module is free to design and change its own internals without external approval. Each module has full authority and ownership over its own domain, its own data, its own code, and its own decisions. The boundary of that authority is the well-defined interface that connects the module to the rest of the system, which is the same role the contract plays in the ownership standard. Authority and ownership are required for the system to work, but they’re required internally, never over another module. A module that tries to exercise authority over another module’s domain, its data, or its decisions has crossed the boundary, and the contract is what makes the crossing visible.
This framing matters because the violations discussed below come from a confused mental model of who owns whom. When an engineer treats a module as an internal database they can read from whenever convenient, the module has been turned back into a department. The discipline is to keep thinking of it as an outside company, and to keep the relationship transactional.
What a Speed Trap Looks Like and Where It Comes From
The most common threat to this discipline is pressure for speed, and it rarely starts with engineers being careless. It comes from three sources, and they tend to compound. A CEO or business leader pushes the tech team to ship faster because the quarter’s ending, a competitor shipped something, or a stakeholder is asking for a release date. A product design hides complexity, so the task as written looks small when it’s in fact a structural change. Or the translation from product requirement to technical task loses the architectural constraints entirely, and the engineer receives a ticket that says “add a username here” with no mention of which domain the username belongs to. In all three cases, the engineer responds most likely with the path of least resistance, which is to add convenience data: passing extra information “just in case” or “so the next module doesn’t have to fetch it.”
The shortcut is forbidden for economic reasons. Developing software without these shortcuts may look more expensive today, because some other module also has to develop something it didn’t need to develop before. The company pays for that extra work now in exchange for never paying the perpetual sync tax on every future change, which is what happens when modules are quietly coupled through “just in case” fields. The trade is between paying once and paying forever. If the company is making good decisions elsewhere, it has the time and money to pay for the “expensive” version, and the long run will be cheaper. The shortcut feels free in the moment and isn’t free at any other moment. There’s also a corporate reflex that makes the trap worse, which is throwing more engineers at the problem. This is a well-known anti-pattern disguised as a solution. When leadership responds to a slow delivery by adding headcount, what they’re actually saying is that the architecture is fine and the people are the bottleneck, and the engineers know that isn’t true. You can staff a tangled system with a hundred engineers and you’ll get a hundred engineers the whole day in meetings stepping on each other. The mark of a mediocre manager is reaching for headcount. The mark of a leader is reaching for the interface, which means fixing the contract, the boundaries, and the data flow, not adding more people into a system that’s already leaking.
Anatomy of a Module-to-Module Request
Every request from one module to another is a small contract, and the contract has three parts. If any part is missing, the receiving module either has to guess (which means it’s no longer independent) or has to fetch data from somewhere it shouldn’t know about (which means the caller has already broken the discipline).
The first part is service identity, which identifies the two modules involved in the conversation. The caller is named and the receiver is named, and ideally the receiver is named with its version, for example payments-v2, so the system knows exactly which service is being asked. Service identity is the equivalent of knowing which company you are calling and which department inside that company is picking up the phone.
The second part is transaction identity, which is a unique ID for the specific request, for example txn-7a3f9b. Transaction identity is what guarantees that if the network fails and the caller retries, the receiver can tell it’s the same operation and not a new one. This is what stops a customer from being charged twice when a payment request times out and the system tries again. It’s the difference between a system that behaves correctly under failure and one that quietly corrupts data.
The third part is subject data, which is the IDs relevant to the work being done. A user_id or an order_id is typical. The crucial point is that these IDs are carried in the transaction, not used to make routing decisions inside the receiver. The receiver doesn’t ask “who’s this user and what tier are they?” The receiver does the job it was asked to do with the data it was given. If the job requires knowing the user tier, that fact has to be in the payload, decided by the caller, and the receiver treats it as input.
Why the Payload Must Be Self-Contained
For a module to be truly autonomous, it mustn’t depend on any other module during its internal processing. This is the part an engineering team gets wrong first, because it requires the most discipline when the temptation to cheat is highest. An engineer on a deadline doesn’t see this pattern as “cheating.” It looks like a common practice.
Before sending a request, the caller gathers all the data required for the transaction and packages it into the payload. If module B needs data from module C to do a job for module A, the flow is this: A calls C, gathers the data, packages it into the payload, and then calls B. From B’s perspective, module C doesn’t exist. Using only the payload and its own database, B processes the request. This sounds like extra work for A, and at the moment it’s, but the whole point of paying it now is that the work isn’t paid again. When module A changes, modules B and C don’t have to change with it, because A gathered the data B needed from C, packaged it into a payload, and handed B a self-contained request. In a tangled system, every change to A forces changes in B and C, and the cost compounds with every modification. In a modular system, the cost of change stays inside A, and the rest of the system doesn’t care.
To see why the extra work is justified, consider a worked example. Consider four modules, A, B, C, and D. For transaction X, A needs data that only C can provide, so A calls C first. A calls C, gets what it needs, builds a payload, and calls B. Using only the payload and its own database, B processes the request and returns a response. Three conditions make this allowed. The data pulled from C has a specific purpose, so it isn’t convenience. The data isn’t being passed to make a decision in B, only to satisfy a processing requirement. B doesn’t use the data on behalf of A. The data serves B’s own processing, which happens to be the job A is paying for.
What’s forbidden is the pattern where A sends a username to B just so B can pass it to C, which is the pattern that sneaks in when engineers are chasing the local minimum. A field is a requirement only when it’s immediately processed by the receiver. Anything else is a forwarding violation, and forwarding violations are how modular systems decay from the inside. A field forwarded once is a debug session next year. A field forwarded across a system is a rewrite in five years.
Ownership, Contracts, and the Cost of Borrowing Data
A module owns its internal logic, full stop. The database schema, the queues, the libraries, the language it’s written in, the way it caches its results, none of that is visible or relevant to the rest of the system. The interface is the contract, and the contract is the only thing the rest of the system is allowed to know about.
This is the shield. As long as the interface remains stable, the module can refactor its internals on a Friday afternoon and no one in the rest of the system notices. If a module decides to split its database, change its primary key format, swap its queue provider, or rewrite its business logic from scratch, the other modules don’t need to change a single line of code. The whole point of paying the cost of a contract is to buy that freedom, and any rule that limits that freedom is a tax on the team’s future.
The mirror image is the responsibility of the caller. If module A “borrows” domain data from module C, which means A reads C’s database directly or pulls a field C didn’t promise to expose, and then C changes its internal structure and A crashes, the fault is entirely A’s. C’s allowed to change. C’s encouraged to change because change is what keeps the system from ossifying. A’s crash is a punishment for breaking the boundary, not an unfortunate accident, and the team responsible for A should be told so in plain terms.
There’s a related question about how strictly to validate the contract, and the right answer is “as strictly as possible at the cheapest moment.” Full schema validation is done at deployment, using a tool that checks the contract between modules before they ship, and one common tool for this is Pact. A well-disciplined practice is to do this check once, on the way in, and trust the result. At runtime, only the request envelope and the coordination mode (synchronous or asynchronous) are validated, because checking the whole contract on every request is wasted work. The contract was already verified. Re-verifying it on every call doesn’t add safety but latency.
A note on versioning, because the instinct is to lean on it. Versioning is itself a moving part, and every moving part is a place where the cost of change goes up and the risk of breakage goes up with it. In an ideal world, a well-designed interface never needs to change, because the function it describes is stable. Adding a new requirement to an existing interface is the wrong move. The right move is to expose a new interface for the new requirement. That said, contracts do change eventually. Business needs evolution, and the system has to evolve with them. The good practice here is to make contract changes expensive and rare, not cheap and frequent, and versioning can too easily become the latter. A team that treats versioning as a license to change contracts casually is a team that has stopped respecting the discipline.
Errors Travel in Codes, Not in Strings
Errors have a source, and the source is the only module allowed to know what the error means in technical terms. The module where a failure originates returns a machine-readable code. For problems inside the module itself, it reports a 5xx code: the database is down, the service crashed, an integration timed out. For problems caused by the caller, it returns a 4xx code: the request was malformed, a required value was missing, the caller isn’t authorized to ask for this. Other modules propagate the code up the chain without translating it, because translating it would require them to know the meaning, and that would couple them to the failing module’s internals.
On-Demand Reporting as an Economic Discipline
In a modular system, relationships between modules are strictly demand-driven. Every interaction is explicit and justified by actual need. If a dashboard, an analytics tool, or an aggregation module wants data, it has to demand it from the service providers. If the demand doesn’t exist, the work to prepare the data is waste, and the waste is paid by the module that shouldn’t have been producing it in the first place.
The temptation to push data is the same temptation as the convenience data in a payload, and it has the same economic shape. An engineer thinks “we have this data anyway, let’s just send it to the warehouse, it might be useful.” And it might be useful, and the report that consumes it might be read by a CEO, and the report might justify a real decision. None of that justifies the cost of producing the data on the off chance. The cost is paid by the producing module on every transaction, for as long as the system runs, for a benefit that may never materialize. If the report is worth building, the report is worth asking for. If the report isn’t worth asking for, the report isn’t worth the cost of feeding.
This is the same logic that justifies the existence of every service in the first place. User data justifies the user module’s existence. The payment module exists because something needs payment processing. The reporting module processes aggregated data because something demands it, and each one of those demands has to come from somewhere real. A demand that exists only in the head of the engineer who “thought it would be useful” isn’t a demand. Treating it as one is how modular systems bloat into data pipelines that pay for themselves in maintenance and pay for nothing else.

What Leaders Actually Have to Defend
Leaders who understand software architecture have to accept that the Autonomous Cooperation Protocol beats headcount, every time. The protocol is what determines whether the team can move fast or whether the team will drown in coordination overhead, and headcount is irrelevant to the protocol. Adding engineers to a tangled system doesn’t untangle it. Adding engineers to a modular system without maintaining the discipline doesn’t stay modular for long. More people means more pressure, more pressure means more convenience data, and more convenience data means the system becomes coupled in ways no one is tracking.
The job of a leader in this context is to protect the constraints. A request to ship faster triggers a question about what the constraint is, not a removal of it. When a request comes in to add a field “just in case,” the discipline calls for refusing and explaining the cost. For a request to throw more engineers at a slow team, ask whether the team is slow because of the team or because of the architecture. The answer is almost always the architecture, and the architecture isn’t fixed by hiring. These aren’t popular answers, and they require a leader who is willing to be unpopular in the short term so the company isn’t slow and unprofitable in the long term. That’s the difference between a manager who focuses on this quarter and a leader who focuses on the next ten years.
References
- Brooks, F. (1975). The Mythical Man-Month: Essays on Software Engineering. Addison-Wesley. Brooks’ Law describes how adding manpower to a late software project makes it later, which is the underpinning of the “throwing more engineers at the problem” anti-pattern discussed in this article. Wikipedia
- Pact Foundation. Pact Contract Testing Framework. Code-first tool for testing HTTP and message integrations using consumer-driven contract tests.
- Fielding, R., Ed. (2022). RFC 9110: HTTP Semantics. Internet Engineering Task Force. Defines the 4xx (Client Error) and 5xx (Server Error) HTTP status code conventions referenced in the “Errors Travel in Codes” section. MDN Web Docs
← Previous: When the Game Changes After Product-Market Fit
Up next in The Engineering Tax · Article 7
Decisions That Look Good Today and Hurt the Company Tomorrow
A feature greenlit to close a deal can look like revenue today and still compress the margin long after the ink is dry. The next article closes that strategy-to-engineering gap without turning the CEO into an engineer. It covers the culture that lets experts speak without fear, the repayment contract on every quick-and-dirty path, and the estimation gap that exposes yes-men when culture alone won't. When the CEO is the single point of failure for whether truth reaches the decision, tomorrow's options hang on those conditions.
Coming soon
Frequently asked questions
How Modules Should Communicate in a Clean Architecture
What is a Domain Interface Contract?
A Domain Interface Contract is the structured agreement between two modules. It has three layers: service identity, transaction identity, and subject data. The caller gathers all required data before making the request, so the receiving module never needs to reach into another system.
What is the Everything Needed Payload Rule?
The Everything Needed Payload Rule says a module must receive all the data it needs in the request itself, not fetch it from other modules during processing. If Module A needs data from Module C to serve Module B's request, Module B must fetch that data from C and pass it to A. From A's perspective, Module C doesn't exist.
How to Find the Right Places to Split a Monolith
How do you find boundaries in a monolithic system?
They don't exist naturally. You must make them. Start with product comprehension: what does the product do? Why do customers buy it? Once you understand this, the data model and API endpoints reveal their hidden structure. Every product is fractal: it consists of smaller products, each with its own supply and demand.
What signals tell you a boundary is wrong?
The clearest signal is borrowed data from another domain. A user's full name stored in the orders table tells you two components are improperly joined. Another signal is margin. When the cost of maintaining a component exceeds the value it creates, that component needs a boundary change: split it, reorganize it, or kill part of it.
Newsletter
Be the first to get next articles in this series.