Skip to content

Modular Monolith Boundaries Done Wrong

Sponsor: Do you build complex software systems? See how NServiceBus makes it easier to design, build, and manage software systems that use message queues to achieve loose coupling. Get started for free.

Learn more about Software Architecture & Design.
Join thousands of developers getting weekly updates to increase your understanding of software architecture and design concepts.


So you built a modular monolith. You have a clean structure. Different projects. Everything broken into modules. But somehow, when you make a change, it still ripples through the rest of your system. Why? Because its highly coupled.

YouTube

Check out my YouTube channel, where I post all kinds of content on Software Architecture & Design, including this video showing everything in this post.

As an example, let’s say you have an HR module. You change an employee’s work status, and all of a sudden, that affects authentication.

Or you’re in a transportation system, and you change a vehicle’s compliance status, and somehow that affects shipment planning.

You defined the modules. So what happened?

You defined boundaries, but you were likely only thinking about boundaries in one way. More specifically, not everything revolves around data alone. And logical boundaries are not physical boundaries.

When you define your modules, you might think you’ve split up your system. But if you still have all this coupling and all these dependencies, did you really split up ownership?

Logical Boundaries Are About Ownership

Typically, when people think about modules in a modular monolith, they’re not that far off from how they think about services and microservices.

They’re usually thinking about the physical aspect.

A process. A database. A queue. A deployable unit. A container. Some network boundary.

But what we’re after, regardless of whether it’s a module in a modular monolith or a service in microservices, is a logical boundary.

We want to think about ownership over behavior.

Who owns the rules? Who owns the lifecycle? Who owns the meaning of something? Who has authority over it?

Physical boundaries do matter. They can enforce isolation and define how modules communicate with each other. For example, maybe you define a public API that determines how one module communicates with another.

But just because you define a physical boundary does not mean it’s the right logical boundary.

And that’s where coupling shows up.

You make a change in one module, and it ripples through the rest of the system because the ownership was never really separated.

Logical boundaries are all about ownership. Who owns the data? Who owns the business rules? Who owns the invariants you need to enforce? Who owns the lifecycle and workflows that happen in the system?

And maybe more importantly, who owns the concept of something?

A module in a modular monolith should be a logical boundary. A service in microservices should also be a logical boundary.

If that’s not making sense, it’s because a logical boundary is not a physical boundary. How you deploy is a different concern.

The Same Word Does Not Mean The Same Concept

A great way to figure out where logical boundaries are is through language.

The same word does not mean you’re talking about the same concept.

Here’s an example.

Let’s say we’re talking about a shipping system where we might have logical boundaries for compliance and dispatch.

A vehicle is not a vehicle.

What I mean by that is, in compliance, you have things like vehicle registration, vehicle insurance, and vehicle maintenance.

But in dispatch, you have the same vehicle, yet it’s a different concept for a different need.

In dispatch, you care about availability. Can you dispatch it to a shipment so it can go to the pickup? What drivers are seated in that vehicle? What’s the capacity of the trailer? Can you get another shipment on that vehicle?

They’re the same vehicle, but they’re not the same vehicle.

It’s the same word. It’s the same thing in the real world. But it’s a different concept. It has a different meaning. It has a different boundary.

If you had only one concept of a vehicle that everything needed to use, guess what happens?

You end up with coupling.

That’s how you make a change to vehicle and affect dispatch, compliance, and billing. Because vehicle is not a singular concept.

I often call this an entity service. You have a single entity that exists in the system, and you try to share that concept across different parts of your business. But those different parts of your system should own their own concept of a vehicle.

An Employee Is Not An Employee

Here’s another example that everybody can probably relate to.

Think about the concept of an employee.

You might have modules for HR and IT.

In IT, you might not even think of this person as an employee. It’s more like a user. That user has authentication, roles, and access to applications.

In HR, that employee means something very different. HR cares about compensation, benefits, onboarding, and employment status.

IT and HR might be talking about the same physical person, but they are entirely different concepts. Each module, each logical boundary, cares about that person in a completely different way.

Now you might be thinking, “But if I only had one model, one source of truth for employee or vehicle, isn’t that better? I don’t have duplication.”

But now you’re coupling two different concepts together into one.

That means if something needs to change in one module because there’s a reason to change there, it can affect another module that has absolutely no reason to change.

That’s the coupling.

One reason to change in one boundary is not the same reason to change in another.

For example, in a shipment system, one boundary might see a vehicle as a compliance asset. That’s the reason it changes there.

Another boundary related to dispatching sees the vehicle as shipment capacity. That’s the reason it changes there.

Not both.

Same thing with employee.

In HR, it’s an employment record.

In IT, it’s a user account.

In a medical system, a patient might be a clinical record in one boundary. But in billing, that same person is a billing account.

They’re not the same reason to change. And that’s another great way, beyond language, to think about where boundaries lie.

Ownership Is Not Integration

There’s a difference between ownership and integration.

Ownership is about having the internal model, the concept, how you define it, and what you expose to other boundaries.

Integration is how other boundaries learn about what they need.

When you lose that distinction and create some shared concept that all boundaries use, you lose the boundary you had to begin with.

You can still integrate. You can expose an API. You can publish events. You can create projections or read models.

But that’s very different from saying, “Here’s one shared model that every module uses.”

Because once every module depends on the same model, one change affects everything else. That’s when everything ripples through your system.

Logical Boundaries Are Not Physical Boundaries

I need to keep enforcing this idea because it matters.

Logical boundaries are not physical boundaries.

If you fundamentally understand this, you’ll look at the last 15 years of microservices, and now modular monoliths, and wonder why we talk about them as if they’re the same type of thing.

They’re different concerns entirely.

Let’s say you have a monolith with logical boundaries A, B, and C. They exist in the same source code repository, and you have a build process that turns everything into a deployable unit.

That’s your modular monolith.

The problem with microservices is there was a tendency to think this way:

“I have a logical boundary. It has its own source code repository. That turns into a deployment artifact. Then each boundary gets deployed separately and communicates over the network.”

Even if those source code repositories were in a monorepo, the mental model was still often:

Logical boundary equals source repository equals deployment unit.

But that’s not the case.

Your boundaries are whatever you define around behavior and ownership. They can be spread across frontend, backend, and infrastructure.

You’re really taking a slice out of what your entire system does.

That means you could have a logical boundary that has source code that turns into two deployable units. Maybe you have a web API and a background processing job that handles messages off a queue or event log.

You could also have a logical boundary that is spread across repositories. Maybe that’s frontend and backend, and they get deployed separately.

Or maybe you have a service that has two source repositories, but the build process puts them into the same deployable unit or container.

A great example is a backend service that also has a mobile app. The mobile app gets built into an APK or AAB for Android, or an iOS app, and maybe that mobile app includes functionality from multiple services.

Logical boundaries are not physical boundaries.

And that also applies to your database.

Database Ownership Still Matters

You might have a database instance with tables owned by one particular boundary. That does not mean another boundary cannot use the same physical database instance.

Maybe two logical boundaries use the same underlying database instance, but each owns its own schema.

Then another boundary might use a completely different database instance.

That’s fine.

The important part is ownership.

Who owns the schema? Who owns the tables? Who owns the data behind the behavior?

Physical separation can help enforce boundaries, but it does not define the boundary by itself.

You Still Have Coupling

You’re going to have coupling between modules.

That’s reality.

The question is how you want that coupling to exist and what you want to expose.

Is it through an API? Through a projection? Through a read model? Through events?

There are different ways to deal with coupling, but it becomes very different when you define ownership clearly.

That’s the difference between having boundaries that own concepts and having one shared model that every module is coupled to.

With one shared model, when one thing changes, everything else is affected. Everything ripples through your system.

Start With Ownership

If you’re defining a new system and trying to create boundaries, start with ownership.

If you’re working in an existing system and the boundaries don’t feel quite right, start with ownership.

Who owns the concept? Who owns the business rules? Who owns the behaviors? Who owns the data behind those behaviors and capabilities?

That’s the logical boundary.

A vehicle is not a vehicle. An employee is not an employee.

The same word does not mean the same concept. And if you miss that, you’ll end up with modules that look clean physically, but are still tightly coupled logically.