Before You Microservice Everything, Read This
Microservices are powerful but often overused. Modular monoliths offer a simpler, scalable way to structure applications, especially at the start of a project.
Join the DZone community and get the full member experience.
Join For FreeThe way we build software systems is always evolving, and right now, everyone's talking about microservices. They've become popular because of cloud computing, containerization, and tools like Kubernetes. Lots of new projects use this approach, and even older systems are trying to switch over. But this discussion is about something else: the modular monolith, especially in comparison to microservices.
But, why focus on this?
Because it seems like the tech world has jumped on the microservice bandwagon a little too quickly, without really thinking about what's driving that decision. There's a common idea that microservices are the perfect solution to all the problems people have with traditional, monolith application systems.
From my own experience working with systems that are deployed in multiple pieces, I know this isn't true. Every way of building software has its good and bad points, and microservices are no different. They solve some problems, sure, but they also create new ones.
First, we need to get rid of the idea that a monolith application can't be well-made. We also need to be clear about what we actually mean by "monolith application," because people use that term in different ways. This post will focus on explaining what a modular monolith is.
Modular Monolith – What Is It?
When we're talking about technical stuff and business needs, especially how a system is put together, it's really important to be precise. We need to all be on the same page. So, let's define exactly what I mean by a modular monolith.
First, what's a "monolith"? Think of it like a statue carved from a single block of stone. In software, the "statue" is the system, and the "stone" is the code that runs it. So, a monolith system is one piece of running code, without any separate parts.
Here are a couple of more technical explanations:
- Monolith System: A single-application software system is designed so that different jobs (like handling data coming in and out, processing information, dealing with errors, and showing things to the user) are all mixed together, instead of being in separate, distinct pieces.
- Modular Monolith Design: This is a traditional way of building software where the whole thing is self-contained, with all the parts connected and relying on each other. This is different from a modular approach, where the parts are more independent.
The phrases "mixed together" and "parts are connected and relying on each other" make single-application design sound messy and disorganized. But it doesn't have to be that way. To sum up, a monolith is just a system that's deployed as a single unit.
Let’s get into a deeper look.
To understand what "modular" means, let's define it. Something is modular if it's made up of separate pieces that fit together to make a whole, or if it's built from parts that can be combined to create something bigger.
In programming, modularity means designing and building something in separate sections. Modular programming is about dividing a program into separate, interchangeable modules, each responsible for a specific task. A module's "interface" shows what it provides and what it needs from other modules. Other modules can see the things defined in the interface, while the "implementation" is the actual code that makes those things happen.
For a modular design to be effective, each module should:
- Be independent and interchangeable.
- Contain everything they need to do their job.
- Have a clearly defined interface.
Let's look at these in more detail.
Independence and Interchangeability
Modules should be as independent as possible. They can't be completely separate, because they need to work with other modules. But they should depend on each other as little as possible. This is called Loose Coupling, Strong Cohesion.
For example, imagine this code:
// Poorly designed module with tight coupling
public class OrderProcessor
{
private readonly InventoryService _inventoryService;
private readonly PaymentService _paymentService;
public OrderProcessor(InventoryService inventoryService, PaymentService paymentService)
{
_inventoryService = inventoryService;
_paymentService = paymentService;
}
public void ProcessOrder(Order order)
{
_inventoryService.CheckStock(order);
_paymentService.ProcessPayment(order);
}
}
Here, OrderProcessor
is tightly linked to InventoryService
and PaymentService
. If either of those services changes, OrderProcessor
has to change too. This makes it less independent.
A better way is to use interfaces to make the modules less dependent on each other:
// Better design with loose coupling
public interface IInventoryService
{
void CheckStock(Order order);
}
public interface IPaymentService
{
void ProcessPayment(Order order);
}
public class OrderProcessor
{
private readonly IInventoryService _inventoryService;
private readonly IPaymentService _paymentService;
public OrderProcessor(IInventoryService inventoryService, IPaymentService paymentService)
{
_inventoryService = inventoryService;
_paymentService = paymentService;
}
public void ProcessOrder(Order order)
{
_inventoryService.CheckStock(order);
_paymentService.ProcessPayment(order);
}
}
Now, OrderProcessor
depends on abstract definitions (IInventoryService
and IPaymentService
), which makes it more independent and easier to test or change.
Modules Must Contain Everything They Need
A module has to have everything it needs to do its job. In a modular monolith, a module is a business module designed to provide a complete set of features. This is called Vertical Slices, where each slice is a specific business function.
For example, in an online store, you might have modules like OrderManagement
, InventoryManagement
, and PaymentProcessing
. Each module has all the logic and parts needed to do its specific job.
Modules Must Have a Defined Interface
A final key to modularity is having a well-defined interface. Without a clear contract, true modular design isn't possible. A contract is the "entry point" to the module, and it should:
- Be clear and simple.
- Only include what clients need to know.
- Stay stable so it doesn't cause problems.
- Keep all the other details hidden.
For example, you can define a module's contract using interfaces:
public interface IOrderService
{
void PlaceOrder(Order order);
Order GetOrder(int orderId);
}
public class OrderService : IOrderService
{
public void PlaceOrder(Order order)
{
// Implementation details
}
public Order GetOrder(int orderId)
{
// Implementation details
}
}
Here, IOrderService
is the contract, showing only the methods needed to work with the OrderService module.
Conclusion
Building a monolith system doesn't automatically mean it's badly designed, not modular, or low quality. A modular monolith is a single-application system built using modular principles. To make it highly modular, each module should:
- Be independent and interchangeable.
- Contain all the necessary parts to do its job (organized by business area).
- Have a well-defined interface or contract.
By following these principles, you can create a modular monolith that has the simplicity of a single application with the flexibility of a modular design. This is especially useful for systems where microservices might make things too complicated.
Opinions expressed by DZone contributors are their own.
Comments