In my previous article, 3 Reasons to Build Monolithic Systems, I recommended starting with a monolithic architecture, for speed and simplicity, if your system is not yet well defined. However, to clarify, starting with a monolith is not a license to start with no architecture and toss the whole thing later!
Monolithic architectures have become nearly synonymous with cobbled, messy code that is difficult to maintain — a “big ball of mud” is a nickname popularized in a talk by Brian Foote and Joseph Yoder way back in 1997. But cobbled and messy is not a requirement. Monolithic typically just means that the code for a system is in a single codebase that is compiled together and produces a single artifact. By this definition, monolithic is more about packaging and distribution than it is about the level of modularity, coupling, or defined interfaces.
Monolithic systems can absolutely be built as a set of loosely coupled, modular components with well-defined interfaces, enabling easier maintenance down the line. So why the bad reputation?
Consider a typical web application with a 3-tier layered architecture. There is a web layer, app or business logic layer, and a database access layer. A common Java packaging scheme would be to group the web stuff, the business logic routines, and the database access routines into separate layer packages as shown on the left side of the diagram below.
The problem is that because each layer is in its own package, each package (or layer) has to expose interfaces as public. Once that is true, there is really no enforcement of who can access those routines, other than by convention. So, in the right hand example above, the CustInterface component can skip the CustApp logic and directly call the CustDataAccess routines. Worse, when a new feature set such as an Order is introduced, all context (and therefore convention) is completely forgotten, public interfaces are called directly, and the big ball of mud (i.e. spaghetti dependencies) begins.
There are other architecture schemes like hexagonal or ports & adaptors, but these turn out to be really just another form of layered architecture where you package the inner layer separate from the outer layer, you still expose public interfaces from these packages, and the same muddy mess ensues over time.
Part of the trouble turns out to be misuse of the access modifiers built into the language. If you make all classes public, for example, then in Java, packages become about code organization, like file folders, rather than about enforcing encapsulation.
A more modular packaging scheme (made even easier if you use modules introduced in Java 9), looks like the following diagram:
In the above packaging scheme, classes implementing business logic and data access are protected (grayed out in diagram above), and all access to these routines are through the defined service interfaces, the only package interfaces made public. Now this diagram should look a little familiar — this scheme is nearly indistinguishable, architecturally, from a microservice.
Using well encapsulated components, a monolith can be written (or easily refactored) to provide loosely-coupled modules, well-defined interfaces, and encapsulated data access. You can also easily substitute one implementation for another if your interfaces remain consistent. A modular monolith will take you a long way toward the more agile architecture promised by microservices.
Microservices do offer additional benefits that may be important to your team. Most importantly, microservices will allow you to version, release, and scale individual services separately, and to permit polyglot programming. As either the scale of your system or the size of your team grows significantly, these benefits will become more important and will eventually outweigh the complexity and inherent latency that microservices necessarily introduce.
But the point is to choose a microservices architecture because these additional benefits are needed and warranted, not to make your code better. If your components and their interfaces are not well-defined, then microservices certainly will not help. You’ll just end up with distributed balls of mud…with added latency!
Resources For Further Reading
I have no ties to any of the resources cited here, I’m just including the links as I found these sites very useful to my understanding.