The Modular Monolith Boundary I Couldn’t Take Back

Modular monolith is one of those architecture patterns everyone recommends and almost nobody documents the failure modes of. Most guidance stops at split by business domain, talk through events, keep modules decoupled. A recent postmortem from Milan Jovanović is a good reminder that a boundary which looks correct on day one can quietly become impossible to walk back a year later. The lesson generalizes well beyond his specific system, and it is worth unpacking in detail.

The System and Why the Split Looked Right

The system in question is a manufacturing ERP with a dealer ordering tool built on top of it, the kind of codebase that has been running for decades and is being modernized in pieces. Two areas of the domain looked obviously separate. A Catalog module owned products, configurations, options and pricing, while a Collaboration module owned the back and forth of building an order together: drafts, comments, approvals and revisions.

Splitting these into two modules was not a reflex decision, it was deliberate. Different responsibilities, different parts of the team working on each, and every individual choice held up fine on its own merit. This is exactly how a modular monolith is supposed to be designed, and if the story stopped here it would read like a textbook example.

The Catalog and Collaboration boundary as it looked on day one, cleanly separated.
The Catalog and Collaboration boundary as it looked on day one, cleanly separated.

How the Coupling Crept In

Trouble did not arrive as an architecture decision, it arrived as ordinary feature work. A collaboration screen needed product options, so it read from the catalog module. Then it needed live pricing too. Then a requirement showed up that an order had to reflect catalog changes immediately.

None of these individually looked like they were reshaping the architecture. But each one added another thread between Collaboration and Catalog, and over months those threads multiplied. The boundary was still visible in the folder structure. It had stopped meaning anything in practice.

This is a pattern worth watching for in your own modules. If you find yourself adding a data lookup just this once across a module boundary every few sprints, that is not a series of unrelated exceptions. It is the boundary telling you something about where the real seams actually are.

The same boundary a year later, with cross module reads and event handlers woven through it.
The same boundary a year later, with cross module reads and event handlers woven through it.

The Assumption That Broke

The deeper issue was not the coupling itself, it was the communication style chosen at the very start. Across the rewrite, modules talked to each other asynchronously through events. That decision kept modules decoupled during a high stakes migration and let the team replace the legacy system piece by piece, which was a completely reasonable trade off to make during a rewrite.

Catalog would publish an event, Collaboration would react whenever it got around to processing it, and the two stayed independent. That held up fine until the business needed an order to be correct the instant a dealer hit save on a configuration change. Eventual consistency had been the right call for every requirement the team knew about when the boundary was drawn. The requirement that broke it simply did not exist yet.

You cannot retrofit immediate consistency onto an asynchronous boundary without a lot of pain. The usual fix is synchronous calls, shared transactions and workarounds bolted on to paper over the gap, which is exactly what happened here. At that point the system was effectively telling the team that these two modules belonged on the same side of the line.

Eventual consistency versus immediate consistency: a bet that quietly stops paying off once a real time requirement shows up.
Eventual consistency versus immediate consistency: a bet that quietly stops paying off once a real time requirement shows up.

Signals That Do Not Look Like Signals

Looking back, the warning signs were all present well before the boundary actually broke. None of them looked alarming in isolation, and that is exactly the problem with this kind of drift.

  • Collaboration constantly reading Catalog’s data was treated as normal cross module traffic, when it was really the boundary asking to be redrawn.
  • Nearly every new Collaboration feature reaching into Catalog felt like healthy growth, right up until it became merge pressure.
  • Event handlers added purely to keep the two modules in sync looked like good event driven design, when they were actually the coupling everyone wanted to avoid, wearing a different costume.
  • The first hotfix demanding immediate correctness got waved off as a one off case, when it was really the eventual consistency assumption starting to crack.

Any single one of these is easy to dismiss. Stacked up over a year, they are the whole story of why the boundary stopped being reversible.

What This Changes About How You Draw Boundaries

A module boundary is a guess made at the point when you know the least about the system. Treat it that way instead of filing it away as a settled decision. Keep testing it as new requirements arrive rather than assuming the first cut was final.

Start with fewer, coarser modules than you think you need. Splitting one module into two later, once the seam is obvious, is a cheap and low risk change. Merging two modules back together after code, teams and mental models have grown around the separation is expensive and risky, and it is the direction most teams end up needing to go in when the initial split turns out wrong.

When you are unsure whether two things belong in one module or two, keep them together and make the module earn its independence over time. It is the same discipline as waiting for a third repetition before you extract a shared abstraction, resisting the urge to design for a generality you have not actually observed yet.

Let consistency requirements draw your boundaries rather than domain diagrams alone. If two pieces of data must be correct at the same instant, they belong on the same side of a module line, regardless of how clean the domain separation looks on a whiteboard. Eventual consistency across a boundary is a bet that no future requirement will ever demand otherwise, and that bet should be made on purpose, not by default because events felt like the modern choice.

Splitting a module later is cheap. Merging two modules back together is the expensive, risky direction.
Splitting a module later is cheap. Merging two modules back together is the expensive, risky direction.

A Practical Way to Catch This Earlier

One thing worth adding from a production standpoint is that you do not have to rely purely on instinct to notice this kind of drift. Architecture tests, using a library like NetArchTest in a .NET codebase, can assert rules such as Collaboration must not reference Catalog’s internal types, and fail the build the moment someone adds that quiet cross module call. Running this in CI turns a slow, invisible erosion into a loud, immediate one, which is a much better failure mode to be woken up by.

This does not replace the judgment call of where to draw the boundary in the first place, and it will not tell you when a consistency requirement is about to change on you. What it does is stop the boundary from silently dissolving without anyone noticing, which is the exact failure this postmortem describes.

The Uncomfortable Part

None of this means modular monoliths are a trap, or that asynchronous messaging between modules was a mistake. Every individual call made along the way was sound given the context available at the time, and that context was incomplete in a way nobody could have seen coming.

The harder lesson is that the boundaries drawn earliest in a project are the ones most likely to be invalidated by requirements nobody knew about yet, and they are also the least likely to get revisited once the codebase has grown around them. The door was not a one way door on the day it was walked through. It became one way gradually, behind the team, one reasonable feature at a time.

Leave a Reply

Discover more from Behind the Stack

Subscribe now to keep reading and get access to the full archive.

Continue reading