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 and tell us what your dream job looks like!