PEDRO OS
← Articles

FIELD REPORT · CODEX

The module that knows too much is not a module. It's a monolith with extra files.

Modularizing a project without modularizing the thinking creates an illusion of isolation. The practical result is a monolith distributed across folders, with invisible coupling and increasingly slow builds.

The module that knows too much is not a module. It's a monolith with extra files.

I have already shipped a modularization that technically worked. We split the project into modules, configured the dependencies, the build compiled in parts, the team was happy. It seemed right.

Six months later, any change to UserProfile was breaking four modules that theoretically should have had no idea each other existed.

It wasn't an implementation problem. It was a thinking problem.


The illusion of the module

When most teams decide to modularize, the initial criterion is almost always topographic: split by feature, by layer, by domain. The project grows, the build gets slow, someone suggests modularization, and the team starts moving files around.

The result tends to be the same: the modules exist in the file system, but the dependencies keep flowing freely through the code.

I saw this on a fintech project. The team had eighteen modules. But the Authentication module imported the UserProfile module, which imported the Analytics module, which imported the Core module, which imported almost everything. Any model change traversed the entire graph. The build went from four minutes to eleven in three months. Not because of code growth. Because of invisible dependency growth.

The modularization was there. The isolation was not.


The real problem is not technical

Most modularizations fail for a reason that never shows up in the architecture diagram: the team modularized the project, but didn't modularize the domain.

Each module keeps thinking about the entire system. Each feature knows details it shouldn't know. Each layer carries context that doesn't belong to it.

When a Payments module needs to know the name of the logged-in user, and goes to fetch that information directly from a shared Auth model, it has created a domain dependency that no technical boundary will erase.

You can put that code in separate modules, with separate targets, separate frameworks, separate repositories. The dependency still exists. It just became less visible.

This is what I call Decorative Modularization: the structure exists to organize files, not to isolate responsibilities.


Why Clean Architecture doesn't solve it on its own

I also thought Clean Architecture would fix this.

If each module follows the dependency rule, if each layer points only inward, isolation comes along with it. At least that's what I believed when I structured the bank account module in a solutions architecture project.

What happened in practice: each module had its own internal Clean Architecture, but the modules talked to each other through domain models. BankAccount, Transaction, UserProfile circulated everywhere. They were shared through a Core module that kept growing without end.

The Clean Architecture was correct inside each module. But between modules, the coupling was total. The architectural boundary existed vertically, not horizontally.

Core became the new monolith.


What changes when you think about boundaries before modules

The real turning point is not deciding how many modules to create. It's deciding what each module has the right to know.

Before creating any target or package, the right question is: what are the knowledge boundaries of the system?

A Payments module has the right to know about payment flow, accepted methods, transaction states. It does not have the right to know about the user's full profile, notification settings, or browsing history.

When that limit is clear in the domain, the module structure emerges naturally. And more importantly: when a dependency violates that limit, it shows up as a bad smell before it becomes a build problem.

In practice, this changes how you define the interfaces between modules. Instead of passing a rich model, you pass exactly what that module needs to know. A PayerID instead of a UserProfile. An AccountSummary instead of a full BankAccount.

It's a small difference in the code. It's a huge difference in maintenance.


The real cost of ignoring this

A system with Decorative Modularization has a predictable behavior over time.

At first, it looks like the structure is working. The modules exist, the team orients itself around them, features are developed in parallel.

At six months, the implicit dependencies start to surface. A shared model changes and the impact is disproportionate. The build starts recompiling modules that should not have been affected.

At one year, nobody can estimate the impact of a change to Core anymore. Delivery speed drops. The team starts to fear touching code that "works."

I've seen this cycle play out more than once. The inflection point is always the same: the moment the team realizes that the modularization created more ceremony without creating more isolation.


What I would do differently

First: I would never start with the targets. I would start with domain mapping. What are the contexts of the system? What information belongs to each context? Where are the natural boundaries?

Second: I would treat the Core module as a warning signal. Every time something goes into Core "because it's shared," that's a domain decision being deferred. In most cases, that code belongs to a specific context. Core grows because it's easy, not because it's right.

Third: I would define the interfaces between modules before defining the internal structure. The external contract of a module is more important than its internal architecture. A module with a well-defined interface can have its internals refactored at any time. A module with a porous interface contaminates everything around it.


What stays

Modularization is an isolation tool. When it isolates nothing, it's just file organization with build overhead.

The question worth asking in any modularized project is not "how many modules do we have?" It's "what does each module have the right to ignore?"

A module that knows too much is not well encapsulated. It's well located. Those are very different things.

And the difference between the two tends to show up exactly when you least want it to: in an urgent refactor, in a data model change, in a new integration that needs to go in fast.

That's when you find out whether you modularized the project... or modularized the thinking.