Super Unicorn Inkmi Logo

Levels of a Modulith - vs Microservices

Benefits of micro services without the downsides

Inkmi is Dream Jobs for CTOs and written as a decoupled monolith in Go, HTMX, Alpinejs, NATS.io and Postgres. I document my adventures and challenges in writing the application here on this blog, tune in again.

Monoliths are bad, and it looks like our industry lately comes to that conclusion too, with the backlash on microservices. The pendulum always swings.

What made Monoliths hated the first time? The problem with Monoliths is highly coupled code. Where every method in every package calls another method in another package until you have a large hairball—or a tangle of USB cables (what is it with cables, whatever you do, and even with a large amount of cable ties). This hairball is hard to understand, hard to move parts around, hard to change and hard to extend. Each team disturbs all the other teams with its changes and coordination and alignment is needed.

How did we get to that? Easy. Business puts pressure on development to deliver faster—they always do. The poor developer reads a ticket which says “Add images of products to the checkout page”, and she starts searching the code base. And finds getImageForProduct(pid) in the directory package. Zing, you have a new dependency from the checkout packages showBasket to the directory packages getImageForProduct(pid) (and the undocumented getImageForProduct might not even work for all products - and you have a bug!).

This goes on and on and on until you have that giant hairball.

Teams were no longer to develop and deliver. Every change created ripples across all teams. All changes created conflicts. All development takes has big alignment and coordination overhead. Something had to change.

Microservices arrived. The application is split up into distinctive chunks. Every part of functionality is packed up into it’s own deployment unit. Every team owns their microservice.

With micro-services you cannot create that hairball (well developers can, with enough pressure, we are a very inventive bunch!). A micro-service has an API surface which can be used by other micro-services. This way the hairball might exist within a service, but not across services (sometimes teams create a meta-hairball by tight coupling micro-services).

Microservices have downsides too. Microservices have development overhead. They add APIs and more code. They duplicate code. They create latency for users. They make debugging flows more complicated.

If you group functionality into modules and introduce a clear API surface to Monolith modules, you get many of the microservice benefits without the complexity.

We call that kind of Monolith a Modulith.

How do you get from your hairball Monolith to a Modulith?

Level 1 Modulith

On the first level of the journey, you group code by domain or area into modules, instead of layering it. Everything that the code needs to run resides in one directory. HTML templates, REST and web controllers, database access logic, configuration, domain and business logic.

Instead of

❯ src
├── controllers/
├── domain/
├── database/

you organize code into

❯ src
├── profiles/
│   ├── conf
│   ├── web
│   ├── templates
│   ├── usecases
│   ├── domain
│   ├── db
│   ├── profile_module.go
├── report/
│   ├── conf
│   ├── web
...

This way everything that is needed for some functionality, like checkout is grouped into one directory. Developers who work on that functionality, do not need to leave that directory. Other teams work on a different directory.

Level 2 Modulith

On the next level you add an (internal) API or contract package to each module.

❯ src
├── profiles/
│   ├── api
│   ├── web

All modules can only call methods and use objects from the api package. You should enforce this with tools that prevent building when the web package of profiles calls into the database package of report (like go arch).

This needs some refactoring and more discipline by developers. This also means additional code - and more work and some inconvenience—and cross-module feature development gets more difficult. You gain more independence of teams for this and fewer bugs because of side effects. The code is easier to understand for newcomers. Modules are easier to refactor and replace.

Level 3 Modulith

On the next level you decouple the modules from each other. Every module that uses another module - calling into the api package - knows about the module. This is still a kind of tight coupling. If the API of another module changes, we need to change.

By introducing an internal message bus over which modules communicate, you decouple modules. No module needs to know about the existence of other modules, just about events that can happen in the system.

This adds further complexity and more code and makes the Modulith less debuggable. But by decoupling modules, each module can faster move independently of others.

Level 4 Modulith

The last level of the Modulith is the most difficult one.

Even if you reject object-oriented programming, you’re probably trained to think in entities in a database. There are customer and order entities. Even microservices often integrate and couple through the database. Modules are coupled through the database if two modules work with the same entities.

You resolve this by making the data local to the module. You break up the customer entity. The billing module owns the invoice address and stores it in a table. The shipping modules owns the shipping address and stores it in a table.

You gain greater independence of modules this way. But all the database thinking tied to entities, the “documents” of NoSQL databases make it even more difficult than a relational model to break from that habit.

Inkmi

At Inkmi I currently run a Golang Level 3 Modulith that uses one database (Postgres). The message bus for integrating modules is NATS.io as in Golang it has the benefit that I can embed it into the application. If app servers don’t need to communicate via message bus, that makes deployments easier. The modules create HTML output, forms are implemented with REST calls instead of formencoded, HTMX and Alpine.js. Way before scaling, but it already makes thinking and developing easier for a solo developer and entrepreneur.

Conclusion

There you have the four levels of a Modulith and how it differs from a Monolith. You reap most of the benefits of microservices with easier developer setup, easier deployments and better debuggability.

If you need microservices for their other benefits like independent scaling and higher resilience, the Level 4 (to a degree Level 3) Modulith can easily been broken into microservices and your architecture migrated to a microservice architecture in a very short amount of time—probably in a week. Automated move each module to its own app and move the internal message bus to an external one. Voilà!

About Inkmi

Inkmi is a website with Dream Jobs for CTOs. We're on a mission to transform the industry to create more dream jobs for CTOs. If you're a seasoned CTO looking for a new job, or a senior developer ready for your first CTO calling, head over to https://www.inkmi.com

Other Articles

©️2024 Inkmi - Dream ❤️ Jobs for CTOs | Impressum