In a monolithic system, it’s too easy to integrate with code from another part of the system. It simplifies initial development but leads to a big ball of mud that’s very hard to maintain.
Big Ball of Mud
I bet most of us have seen systems like this.
There’s no clear architecture. Every part of the system knows everything about every other part of the system. There are no layers. No abstractions. No facades.
It’s even possible that some code accesses properties of classes directly bypassing accessor methods. That makes it hard to refactor a property from being a real one to a calculated one.
It’s also common to see code that accesses any part of the database bypassing code that’s responsible for each of those parts. That makes it hard to refactor the database.
Big ball of mud systems are very hard to maintain. A developer changes a little piece of code in one end of the system and something breaks in multiple distant sides of the system you have no idea about.
Hence when a developer needs to change a piece of code or database, they have to learn a gazillion parts of the system that are coupled to the part they’re working on. Depending on the size of the system, it might take a huge amount of time. And even doing that doesn’t guarantee that something won’t break in a distant side of the system because the developer missed some unexpected usages of the part they’re changing.
Monoliths Make It Easy to Violate Boundaries
If you have a monolithic system, the whole codebase is in the same repository. Every part of the system is out there in the open.
A developer working on a feature sees a class that solves a part of the problem they’re working and they either add to that class, extend it, or use it directly by adding some code around it to get the result they need. Sometimes it is a proper solution of the problem at hand, but most of the time it’s not and it just increases unnecessary coupling between system components.
The same happens to the database.
In the case of a monolith, most often it’s just a single relational database with dozens or hundreds of tables. Sometimes there’s also one or two NoSQL databases added to the mix. But all of them still have full access from any part of the codebase.
A developer sees a table that seems to have data about an entity their problem is related to and they add a couple more columns. Or they create a new table with a foreign key linking to the original table. Or they write a query with a couple dozen of JOINs. Or they start writing data to a table that’s supposed to be managed by another part of the system.
Easy-peasy. A developer solves their problem at hand, gets proud of themselves for a few moments, and gets back to work to solve the next problem.
Have a few full-time developers doing that for a couple years and your system turns into a big steaming ball of stinking mud.
Solve It with Discipline and Training?
Sure, it is possible to avoid all these problems if you have a strong architect that has authority to reject any change to the codebase and database until developers do things properly.
But depending on how many developers there are and how much code they write, it might be unrealistic to expect the architect to be able to catch all those problems.
It also requires a lot of training. Most developers today don’t bother learning proper architecture and design. They just learn a technology stack like LAMP or MEAN and start coding away. It works out fine on their toy projects, but once they get a job and start working on a real system, they have to learn a ton of stuff and unlearn their bad habits.
They need to be trained by someone experienced like an architect or a senior developer. In reality though, most of the time new developers join a project and start cranking out code right away. Because most business don’t have time nor money to invest into training their staff.
Even though discipline and training might work, the best solution I’ve seen so far are microservices.
Microservices Provide Clear Boundaries
Unlike monoliths having one huge repository of code, each microservice has its own repository. That alone already solves the code coupling problem because a developer can’t just go and start using a class that’s not in the repository they’re working on.
When doing microservices properly, each microservice also has their own database or a set of databases. No outside access is allowed.
Now a developer can’t just go and add a few columns to a table they don’t own or write a huge JOIN that links half the database in one query.
The only way a developer can make their codebase get data from a microservice or write data into it is by using the microservice’s public API. It could be a REST API, Kafka topics, or something else, but that’s the only way to get data in and out of the microservice.
When a developer needs something that a microservice doesn’t provide, they now have to communicate with the owner of the microservice to decide how to solve that problem.
The owner might reject solving that problem if it’s beyond the scope of their microservice. Or they might add missing stuff to the microservice and expose it via its API.
All that gives owners of microservices means to avoid problems like scope creep or tight coupling on both the codebase and database levels. Other developers can’t just bypass them and start integrating with their system in ways owners never intended to. It empowers the owner of each microservice to maintain high quality of their piece of the overall system.
Also an owner of a microservice can refactor its inner workings at any moment without having to learn a gazillion of systems that use the microservice’s API.
They might switch to a better database, upgrade to the latest version of the stack, or just freely move classes around. Their only responsibility to other systems is maintaining their API behavior. Everything else is completely up to them.