Three months into the build, Thabo Molefe opened the solution for the fictional Magaliesberg Provincial Supplier Desk and found what he expected to see: six folders named Registration, Documents, Verification, Notifications, Cases and Reporting. Then he searched for every place the Cases code used a class from Verification. There were 41. Some went through the Verification module's service interface. Most did not. Cases code was reading Verification's database entities directly, calling an internal helper that formatted rejection reasons, and in one place updating a verification status to "re-review" when a supplier reopened a case.
Nobody had done anything malicious. Each shortcut saved someone an hour. Together they meant that the "modular monolith" was a monolith with folders, and that the boundary the team had promised the steering committee in Part 1 did not exist in the code.
The Magaliesberg Provincial Supplier Desk is a fictional provincial supplier-support platform built by one delivery team. Its people, systems and figures are illustrative. This part explains what turns a folder into a module, and how to stop the boundary eroding.
Folders organise files. Modules organise dependencies.
A folder tells you where code lives. A module tells you what other code is allowed to depend on. The difference is invisible on the first day and decisive by the hundredth.
On the left, any class can reach any other class, and any table is one query away. The folder names suggest structure, but the compiler, the database and the reviewer all treat the application as one thing. On the right, each module exposes a small contract, hides everything else and owns its tables. Other modules can only use the contract.
The practical test is simple: if you deleted every class in a module except its contract, would the rest of the application still compile? If the answer is no, the other modules depend on internals, and you do not yet have a module.
The four parts of a real module
The Supplier Desk team rewrote its module definition as four parts. The structure echoes how some frameworks describe modules. The Spring Modulith reference documentation (version 2.1.1, checked on 5 November 2026), for example, describes a provided interface, internal components and a required interface. The idea applies to any stack.
- Provided contract. The operations and events the module offers to others. For Verification:
RequestVerification,GetVerificationOutcome, and the eventSupplierVerificationCompleted. The contract uses the module's own vocabulary and its own data types, not database entities. - Internals. Everything else: domain logic, persistence, helpers, adapters. Internals may change on any commit without telling anyone.
- Owned data. The schema the module alone reads and writes. Other modules never see these tables.
- Required contract. The other modules' contracts this module is allowed to call, declared explicitly. Verification may call Registration's
GetSupplierProfile. It may not call Cases at all.
That last part is often missed. A module that declares only what it offers, and not what it needs, can grow new dependencies silently. Declaring the required side makes every new dependency a visible decision.
Dependency direction matters more than dependency count
Some dependencies between modules are healthy. Cases needs to know whether a supplier is verified. The question is which way the arrow points and whether arrows form loops.
The Supplier Desk adopted three rules:
- No cycles. If Cases depends on Verification, Verification may not depend on Cases, directly or through a third module. Cycles mean two modules can only change together, which is the opposite of a boundary.
- Commands go down, events go up. When Cases needs Verification to act, it calls Verification's contract. When Verification needs others to react to an outcome, it publishes an event and does not know who listens. That is how the "re-review" status update was fixed: Cases now calls
RequestReverification, and Verification decides what that means for its own status. - Shared kernel, kept tiny. A few types really are shared: a supplier reference number, a money type, a date range. They live in a small shared library that changes rarely and contains no business rules. If the shared kernel grows, boundaries are leaking into it.
Data ownership is part of the module, not a later step
Most boundary erosion in a modular monolith happens in the database, because a join is the easiest shortcut there is. The Supplier Desk's 41 dependencies included eleven direct queries on Verification's tables.
The team made three changes:
- Each module got its own database schema, and each module's data-access code connects with a database role that can only use that schema. A cross-module query now fails at run time, not at review time.
- Where Cases needed verification data for its screens, it either calls
GetVerificationOutcomeor keeps a small copy updated fromSupplierVerificationCompletedevents. - Reporting, which genuinely needs data from every module, was meant to get it through a controlled read path rather than by joining live tables. Rewriting the monthly report could wait, so as a stopgap it kept one read-only role across every schema. Part 4 tells how that stopgap ended.
Part 4 goes deeper into shared databases, migrations and reporting access. The point here is that a module without owned data is not a module. If two modules can write the same row, they are one module with two names.
Enforce it, or it will decay
A boundary that relies on code review alone will erode. Reviewers are busy, shortcuts look harmless in a diff, and the person reviewing may not know which module owns which class. A boundary drawn only on a slide is a claim, not a fact, which is the argument of The architecture is not the diagram and Architecture diagrams should be evidence, not artwork. The Supplier Desk enforces boundaries in layers, from cheapest to most thorough.
| Layer | What it catches | Supplier Desk example |
|---|---|---|
| Language visibility | Calls to internal classes | Each module is a separate .NET project; internals are marked internal; only the contract project is referenced by others |
| Project references | Undeclared module dependencies | The solution only allows references listed in each module's manifest |
| Architecture tests | Cycles, forbidden namespaces, entity leakage | Unit tests that fail when one module's namespace depends on another's internals |
| Database permissions | Cross-schema queries | One database role per module, with access to its own schema only |
| Contract checks | Breaking changes to a published contract | The contract's public surface is compared with the last release in the build |
Tools exist for the architecture-test layer in most stacks. ArchUnit is a library for "checking the architecture of your Java code" in ordinary unit tests, and ArchUnitNET is a C# library published under the Apache License 2.0 (both checked on 5 November 2026). These are dated examples, not recommendations over alternatives. Part 8 of this series turns the table above into a full set of architecture fitness functions that run in the pipeline.
Signs your modules are only folders
- A change to one module regularly requires changes in two others.
- Database entities from one module appear in another module's method signatures.
- People describe a class as "shared" because nobody is sure who owns it.
- The shared library is growing faster than any module.
- Reporting queries join tables from four or five modules.
- Nobody can say, without searching, which modules a given module depends on.
Each sign has the same cure: decide which module owns the thing, give the others a contract, and make the build reject the shortcut. It is cheaper to do this now than during an extraction, when the five tests will expose every hidden join at once.
What the Supplier Desk changed in two sprints
Over two sprints, the team reduced the 41 cross-module dependencies to nine, all through published contracts. They moved two helpers that were genuinely shared into the shared kernel and deleted a third that turned out to be dead code. They split each module into a contract project and an internal project, added database roles per schema and wrote eight architecture tests. Two of those tests failed on the first run, which was the point.
The nine remaining dependencies were not removed, because they were legitimate: Cases genuinely needs to know a supplier's verification outcome, and Documents genuinely needs to know whether a supplier is registered. The aim was never zero dependencies. It was dependencies that are declared, point in one direction and go through a contract the owning module controls.
Feature delivery slowed for those two sprints. Since then, the team has been able to change Verification's internal rules twice without touching Cases, which was not possible before. The figures are illustrative, but the shape is common: enforcement costs a little now and saves a great deal later. Our article on keeping systems fit in the age of cheap code makes the wider case for that kind of continuous upkeep.
Key points
- A folder organises files. A module organises what other code may depend on.
- A real module has a provided contract, hidden internals, owned data and a declared required contract.
- Forbid cycles, send commands down and events up, and keep any shared kernel tiny and free of business rules.
- Data ownership belongs inside the module from the start; a module that shares writable tables is not a module.
- Enforce boundaries with language visibility, project references, architecture tests, database permissions and contract checks, not code review alone.
Previous: Part 1, Your new system probably does not need microservices. Next: Part 3, Local calls became network calls. Now what?. All parts are on the series page.
How CloudNala can help
CloudNala reviews modular monoliths for boundary erosion. We map the real dependency graph against the intended one, identify cross-module data access and cycles, define contracts and database roles per module, and set up the architecture tests that make the build reject shortcuts, so that your modules stay modules as the codebase and the team grow.
Work with CloudNala
CloudNala helps organisations move from technology ambition to practical execution across cloud, AI, data, platform engineering and digital services.
Whether you are exploring AI, modernising your cloud environment, building a public-sector digital service, or turning an idea into a working MVP, we can help you shape the roadmap and deliver the next step.
Book an Architecture Complexity Review or write to us at consult@cloudnala.co.za
