A look into the Modular Monolith

Go full circle with better engineering standards

Photo by Markus Spiske on Unsplash

As architectures go, the term monolith is synonymous with legacy. These days, if you aren’t using the latest in microservices patterns and deploying using the newest mesh technology, well, you’re not doing it right.

I think microservices have become a bit of a magic hammer, our saviour and cure-all for all the pain and suffering we have endured maintaining years-old monolithic applications. There is a place for microservices, to be sure, but I believe it may be at the end of a journey — and a modular monolith might be the best way to get there.

What is a Modular Monolith?

True to its name, we are talking about a single deployable (monolith) that can be scaled as needed —yes even monoliths can be 12-factor and scale well. Within the monolith, we define modules, each with its own domain and build-system to allow a team to work independently whilst allowing us to package a single deployable.

The modular monolith’s beauty is that the software architecture looks surprisingly similar to the microservices architecture you might draw if you were so inclined.

A huge issue that I see in legacy monolithic architecture is that eventually, teams tend to start to cut corners; engineering principles such as SOLID take a back seat to deadlines and delivery pressure. Under these conditions, tech-debt accumulates, and it can get increasingly harder and harder to control, ultimately leading us to the broken windows theory. Internal dependency management is almost non-existent, and the temptation to cross boundaries to get something across the line is almost too easy (even when an engineer recognises appropriate boundaries).

By building a monolith out of discrete modules, we have an opportunity to create smaller, more efficient and targeted pieces of software. We can think about how a module should behave, and like its microservice counterpart, we can design its public API to keep it as concise as possible. In the case of a Java monolith, we can be more careful about what we place in public interfaces and be more diligent using more protected levels of code within the module itself, so we don’t bleed functionality out into the broader system. The build system enforces dependencies between modules, so there is no chance of circular dependencies between modules being created.

A Java Example

Take your classic monolithic java application — we will essentially have a single build file (Maven / Gradle), and all of our production code stashed away under /src/main/java. You’ll probably have a base package name, and will either group your code under functional or domain principles.

Here we have a simplified version of the classic bookstore example. We have three domains, users, books and orders.

When everything is under a single source root, an engineer can create circular dependencies, JPA is notorious for making this an easy option with one-to-many bidirectional relationships. Apart from linting rules at build time, the compiler will allow any dependency across packages.

Circular dependencies in your code is a massive blocker in the road to microservices (and good software design), creating clear bounded contexts and not creating these dependencies will enable you to potentially break out a service.

By splitting the code out into independent modules using a multi-module project we can use the build system to enforce that the orders domain can reference books but not the other way around. Why would books ever need to know about orders or users for that matter?

Gradle or Maven will not allow you to create a dependency from books to orders, so our bounded contexts are clear and enforced by the compiler.

Since each component has its own build.gradle file it can be run independently. This means any team working on a particular domain have the ability to run test suites particular to their domain, reducing the feedback loop and giving full control back to the team.

Domain-Driven Design

Photo by Hugo Rocha on Unsplash

At the heart of both microservices and modular monolith design should be DDD, and ideally, use the Event Storming process to help find application seams or boundaries. A significant issue with a greenfields project that starts with a microservices architecture is that we often draw these boundaries incorrectly. The cost of refactoring a series of microservices to fix this can be quite costly, nullifying our “fast and cheap to evolve” argument of microservices.

In a Modular Monolith, it can be as easy as a refactor in your favourite IDE! (an oversimplification, I know.)

DDD and Event storming is a topic in its own right; definitely look out for the great content to be found about the topics.

Some problems with Microservices Architecture

I do like microservices architecture; however, there are some issues that I sometimes find on the execution side of things.

People & Processes

The longer I stay in technology, the more I find that resolving issues around people and processes pay dividends at a much higher rate than throwing technology at a problem.

Here are some of my experiences:

  • Teams are not given sufficient time or access to SME’s to define service boundaries correctly, DDD, Event Storming etc.
  • Lack of experience in distributed systems can mean a steep learning curve, and teams pick up the architecture because it’s so hot right now.
  • Lack of time to properly test the architecture to the level of safety needed to deploy a complex system (and nobody can argue that distributed systems aren’t inherently complex).
  • On a greenfields project, it is often one team working on the application, so any argument about scaling people more efficiently using microservices is null and void.

Broken Promises

Microservices claim to solve so many problems we have with software development and deployment; these are just a few things I have consistently found:

  • Breaking apart services because of “scalability” concerns before reaching production is often a premature optimisation. We don’t really know until we get to production if a particular domain is so dominant.
  • The architecture does not guarantee loose coupling of services, and we often see Service A being wholly dependant on Service B (and have to be deployed together).
  • Making things distributed means we have to worry about transport, security, reliability and resiliency — things we didn’t have to worry about as much in a single executable.
  • And for a refactor, well, microservices will not fix a poorly designed organic monolith without some serious engineering.

So now it’s a nightmare. Now the codebase is so bad, and you say,

“You know what we should do? We should break it up. We’re gonna break it up and somehow find the engineering discipline we never had in the first place.”

— Kelsey Hightower, Monoliths are the future

When to explore a Modular Monolith

Modernisation

If you already have a successful monolithic application running, the modular monolith is the perfect architecture to help you refactor your code to get ready for a potential microservices architecture. Often we look to microservices as a silver bullet to tackle the friction we see in delivery — slow to build, test, and release new features; you can start to see these things speed up with a well-factored modular monolith!

Easier said than done. An established monolith will be rife with circular dependencies, poorly named services and god objects (see quote above). It will take real engineering discipline to get to the point of even a half-decent modular monolith, let alone a microservices architecture. But it is a process you can follow to rebuild the plane while you’re flying it.

As with any modernisation effort, you will want to ensure that you have invested sufficiently in a robust regression suite to avoid disappointment.

Green Fields

A modular monolith will allow you the space to learn your domain and pivot your architecture much faster than a microservices architecture (contrary to popular belief).

You will not have to worry about things like Kubernetes and a services mesh. Your deployment topology will be drastically simplified, and you can move those problems until much later in your delivery — perhaps even once you are profitable!

Wrapping up

Nothing in this industry is a silver bullet. I find the idea of a modular monolith a promising software architecture with many of the pros of microservices without many of its issues and complexities.

The path of a Modular Monolith does not block your path to microservices, in fact, it only strengthens your position to prepare for an eventual breakup.

We need to focus more on good software architecture and engineering practices such as SOLID in our design and everyday development. You can achieve a lot with a well constructed (read: modular) monolith without relying on microservices to enforce service boundaries, as some more folks would have it.

Let me know your thoughts!


A look into the Modular Monolith was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.

Go full circle with better engineering standards

Photo by Markus Spiske on Unsplash

As architectures go, the term monolith is synonymous with legacy. These days, if you aren’t using the latest in microservices patterns and deploying using the newest mesh technology, well, you’re not doing it right.

I think microservices have become a bit of a magic hammer, our saviour and cure-all for all the pain and suffering we have endured maintaining years-old monolithic applications. There is a place for microservices, to be sure, but I believe it may be at the end of a journey — and a modular monolith might be the best way to get there.

What is a Modular Monolith?

True to its name, we are talking about a single deployable (monolith) that can be scaled as needed —yes even monoliths can be 12-factor and scale well. Within the monolith, we define modules, each with its own domain and build-system to allow a team to work independently whilst allowing us to package a single deployable.

The modular monolith’s beauty is that the software architecture looks surprisingly similar to the microservices architecture you might draw if you were so inclined.

A huge issue that I see in legacy monolithic architecture is that eventually, teams tend to start to cut corners; engineering principles such as SOLID take a back seat to deadlines and delivery pressure. Under these conditions, tech-debt accumulates, and it can get increasingly harder and harder to control, ultimately leading us to the broken windows theory. Internal dependency management is almost non-existent, and the temptation to cross boundaries to get something across the line is almost too easy (even when an engineer recognises appropriate boundaries).

By building a monolith out of discrete modules, we have an opportunity to create smaller, more efficient and targeted pieces of software. We can think about how a module should behave, and like its microservice counterpart, we can design its public API to keep it as concise as possible. In the case of a Java monolith, we can be more careful about what we place in public interfaces and be more diligent using more protected levels of code within the module itself, so we don’t bleed functionality out into the broader system. The build system enforces dependencies between modules, so there is no chance of circular dependencies between modules being created.

A Java Example

Take your classic monolithic java application — we will essentially have a single build file (Maven / Gradle), and all of our production code stashed away under /src/main/java. You’ll probably have a base package name, and will either group your code under functional or domain principles.

Here we have a simplified version of the classic bookstore example. We have three domains, users, books and orders.

When everything is under a single source root, an engineer can create circular dependencies, JPA is notorious for making this an easy option with one-to-many bidirectional relationships. Apart from linting rules at build time, the compiler will allow any dependency across packages.

Circular dependencies in your code is a massive blocker in the road to microservices (and good software design), creating clear bounded contexts and not creating these dependencies will enable you to potentially break out a service.

By splitting the code out into independent modules using a multi-module project we can use the build system to enforce that the orders domain can reference books but not the other way around. Why would books ever need to know about orders or users for that matter?

Gradle or Maven will not allow you to create a dependency from books to orders, so our bounded contexts are clear and enforced by the compiler.

Since each component has its own build.gradle file it can be run independently. This means any team working on a particular domain have the ability to run test suites particular to their domain, reducing the feedback loop and giving full control back to the team.

Domain-Driven Design

Photo by Hugo Rocha on Unsplash

At the heart of both microservices and modular monolith design should be DDD, and ideally, use the Event Storming process to help find application seams or boundaries. A significant issue with a greenfields project that starts with a microservices architecture is that we often draw these boundaries incorrectly. The cost of refactoring a series of microservices to fix this can be quite costly, nullifying our “fast and cheap to evolve” argument of microservices.

In a Modular Monolith, it can be as easy as a refactor in your favourite IDE! (an oversimplification, I know.)

DDD and Event storming is a topic in its own right; definitely look out for the great content to be found about the topics.

Some problems with Microservices Architecture

I do like microservices architecture; however, there are some issues that I sometimes find on the execution side of things.

People & Processes

The longer I stay in technology, the more I find that resolving issues around people and processes pay dividends at a much higher rate than throwing technology at a problem.

Here are some of my experiences:

  • Teams are not given sufficient time or access to SME’s to define service boundaries correctly, DDD, Event Storming etc.
  • Lack of experience in distributed systems can mean a steep learning curve, and teams pick up the architecture because it's so hot right now.
  • Lack of time to properly test the architecture to the level of safety needed to deploy a complex system (and nobody can argue that distributed systems aren’t inherently complex).
  • On a greenfields project, it is often one team working on the application, so any argument about scaling people more efficiently using microservices is null and void.

Broken Promises

Microservices claim to solve so many problems we have with software development and deployment; these are just a few things I have consistently found:

  • Breaking apart services because of “scalability” concerns before reaching production is often a premature optimisation. We don’t really know until we get to production if a particular domain is so dominant.
  • The architecture does not guarantee loose coupling of services, and we often see Service A being wholly dependant on Service B (and have to be deployed together).
  • Making things distributed means we have to worry about transport, security, reliability and resiliency — things we didn’t have to worry about as much in a single executable.
  • And for a refactor, well, microservices will not fix a poorly designed organic monolith without some serious engineering.

So now it’s a nightmare. Now the codebase is so bad, and you say,

“You know what we should do? We should break it up. We’re gonna break it up and somehow find the engineering discipline we never had in the first place.”

— Kelsey Hightower, Monoliths are the future

When to explore a Modular Monolith

Modernisation

If you already have a successful monolithic application running, the modular monolith is the perfect architecture to help you refactor your code to get ready for a potential microservices architecture. Often we look to microservices as a silver bullet to tackle the friction we see in delivery — slow to build, test, and release new features; you can start to see these things speed up with a well-factored modular monolith!

Easier said than done. An established monolith will be rife with circular dependencies, poorly named services and god objects (see quote above). It will take real engineering discipline to get to the point of even a half-decent modular monolith, let alone a microservices architecture. But it is a process you can follow to rebuild the plane while you’re flying it.

As with any modernisation effort, you will want to ensure that you have invested sufficiently in a robust regression suite to avoid disappointment.

Green Fields

A modular monolith will allow you the space to learn your domain and pivot your architecture much faster than a microservices architecture (contrary to popular belief).

You will not have to worry about things like Kubernetes and a services mesh. Your deployment topology will be drastically simplified, and you can move those problems until much later in your delivery — perhaps even once you are profitable!

Wrapping up

Nothing in this industry is a silver bullet. I find the idea of a modular monolith a promising software architecture with many of the pros of microservices without many of its issues and complexities.

The path of a Modular Monolith does not block your path to microservices, in fact, it only strengthens your position to prepare for an eventual breakup.

We need to focus more on good software architecture and engineering practices such as SOLID in our design and everyday development. You can achieve a lot with a well constructed (read: modular) monolith without relying on microservices to enforce service boundaries, as some more folks would have it.

Let me know your thoughts!


A look into the Modular Monolith was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.


Print Share Comment Cite Upload Translate
APA
Dean Baker | Sciencx (2024-03-28T10:46:39+00:00) » A look into the Modular Monolith. Retrieved from https://www.scien.cx/2021/03/30/a-look-into-the-modular-monolith/.
MLA
" » A look into the Modular Monolith." Dean Baker | Sciencx - Tuesday March 30, 2021, https://www.scien.cx/2021/03/30/a-look-into-the-modular-monolith/
HARVARD
Dean Baker | Sciencx Tuesday March 30, 2021 » A look into the Modular Monolith., viewed 2024-03-28T10:46:39+00:00,<https://www.scien.cx/2021/03/30/a-look-into-the-modular-monolith/>
VANCOUVER
Dean Baker | Sciencx - » A look into the Modular Monolith. [Internet]. [Accessed 2024-03-28T10:46:39+00:00]. Available from: https://www.scien.cx/2021/03/30/a-look-into-the-modular-monolith/
CHICAGO
" » A look into the Modular Monolith." Dean Baker | Sciencx - Accessed 2024-03-28T10:46:39+00:00. https://www.scien.cx/2021/03/30/a-look-into-the-modular-monolith/
IEEE
" » A look into the Modular Monolith." Dean Baker | Sciencx [Online]. Available: https://www.scien.cx/2021/03/30/a-look-into-the-modular-monolith/. [Accessed: 2024-03-28T10:46:39+00:00]
rf:citation
» A look into the Modular Monolith | Dean Baker | Sciencx | https://www.scien.cx/2021/03/30/a-look-into-the-modular-monolith/ | 2024-03-28T10:46:39+00:00
https://github.com/addpipe/simple-recorderjs-demo