If you are a creator giving birth to a new web application, you may be racking your brain trying to uncover the right way to do it.
In a previous article, we got into back-end architecture and discussed the monolith vs. microservices dichotomy.
We saw the benefits and drawbacks they each have for creating a new app and took a look at the approaches used by various remarkable players in the market.
If you read it, you may be already leaning towards a monolithic architecture for your app at this point.
Or maybe not. And that’s ok. No two situations are equal, and perhaps yours calls for a distributed system.
Hopefully, whatever the case, it’s made you reflect and avoid making your life unnecessarily harder.
Yet, if you believe the monolith is the way for you, by now, you should already know that’s not enough. Taking up the gauntlet thrown by DHH, it also has to be majestic.
But how can your monolithic app become majestic? And what’s a majestic monolith, by the way?
The Majestic Monolith defined.
Let’s start with a question. Which of the constructions below is a majestic one?
They were built in times very distant from each other, but if you see it as I do, both are equally glorious. You may like one over the other, but one thing is obvious: they both are monumental living symbols of history.
Still, they hold a difference. One is monolithic, while the other is not.
In the same vein, a software application might not be monolithic and be equally magnificent. And, monolithic or not, it might be a botched job as well.
The quality of majesty has nothing to do with the monolithic nature of a system.
On the other hand, majesty does not have as much to do with an imposing external aspect either.
Rather, we can say a software monolith becomes majestic when it makes life better for its users and makers alike.
It becomes majestic when it remains easy to understand and work with. It gets majestic when it can quickly evolve smoothly and consistently with the business and the rest of its environment.
And in my opinion, it goes even more majestic when it does it at the lowest possible cost, not just technically, but organizationally, when it doesn’t put on your shoulders more burden than you strictly need.
But yeah, I know. This definition doesn’t tell you much about how you should actually build your app, right?
The question is, how can you make these properties a reality in your application? How can the Majestic Monolith manifest in some form?
Against the general current of microservices, a new architectural term has grown in popularity inside the tech community in the last few years.
That concept is the Modular Monolith. But what does modular mean?
According to Wikipedia:
Modular programming is a software design technique that emphasizes separating the functionality of a program into independent, interchangeable modules, such that each contains everything necessary to execute only one aspect of the desired functionality. A module interface expresses the elements that are provided and required by the module. The elements defined in the interface are detectable by other modules. The implementation contains the working code that corresponds to the elements declared in the interface.
But from this explanation, isn’t a layer also a module in a layered architecture? And isn’t a math library a module too?
We might say that almost all systems, monolithic or not, are modular to a greater or lesser extent, right?
Modularity is a way to encapsulate a program’s functionality in different chunks, but it doesn’t say anything about how we should split up that functionality.
It can get us closer to majesty, but the term leaves so much ambiguity on the table yet.
Welcome the Domain-Driven Monolith.
A Domain-Driven Monolith is a particular form of Modular Monolith.
Yet, in this kind of monolith, there is this distinct idea of logically splitting the application into vertical slices.
These vertical slices do provide business, not technical functionality. They are independent services that comprise everything required to deliver a given business capability —including data, infrastructure, domain logic, APIs, etc.
Essentially, each vertical slice represents a BoundedContext and is exclusively responsible for a given business concern.
Instead of the typical layers expanding across the whole application surface, we split the application into different spaces of behavior, reducing coupling between its various parts and improving cohesion inside them.
To get a clearer picture, look at the image below.
Looks familiar? If you’ve worked with microservices before, this partitioning is similar to the one we’d find in a well-designed microservices/SOA system.
Yet, you’re looking at a monolith. And as in microservices/SOA, those business services share powerful properties:
- They are self-contained.
- Operate as a black box to other consuming services.
- Communicate with each other only through explicit boundaries.
- Are loosely coupled with the rest of the system.
That’s the nice point. Holding services within the same process helps us keep the good parts of microservices while overcoming most of the pitfalls induced by distribution.
Does a business rule change? Alright. I’ll simply modify module A. Need to refactor the boundaries between two services? Easy, I’ll just do a local refactoring. A new team member just got on board? No problem. Since everything is clearly organized and technical complexity is smaller, they’ll quickly get up to speed.
This approach is nothing new. In reality, it’s just Domain-Driven Design constrained to a single process.
I started applying this style at Deebbler a few years ago. And since then, this has been my go-to approach for creating new apps.
Make your app look thinner.
Traditionally, the usual was to shape applications through single unified models.
However, with that approach, as complexity grows, concepts in the model start attempting to represent too many different things from very different contexts simultaneously. The model grows bloated and inconsistent and soon turns into a tangled whole (a.k.a. Big ball of mud).
As a result, working on the app gets increasingly confusing, and making progress becomes a true odyssey.
Here, however, each slice has its model, a smaller, simpler, and clearer model. It’s a more straightforward system to comprehend. It feels as if you were working on a thinner app with far less complexity involved.
Order a la carte.
Below you can see a slice’s internals based on CQRS and its integration with other external slices in the same app. It is an excerpt from an actual application I developed some years ago.
The diagram shows the design inside a slice, but other slices can be radically different. And that’s another one of the funny things. You can pick the best internal architecture for the job at hand for each slice.
Are you only making an early prototype for a new feature set? You can experiment and learn further with a transaction script or CRUD model for that service.
Do you have another component that requires complex queries or strict auditing? Go with CQRS or Event Sourcing.
Another set of core features calls for a richer, loosely-coupled model but doesn’t require such a complicated solution? You can stay with the more classical layered or hexagonal design.
You don’t need to stick to a single plan for the whole system. That’s the beauty in it. Use what the situation demands —and nothing else.
Some remarkable-sized companies have been drinking from this philosophy.
Take the example of Root. They are a young US-based insurtech startup that initiated its journey in 2015. They began with a tangled monolith, but a year in, they realized they had to consider alternative architectures.
As Dan Manges, their co-founder and CTO, splendidly explained in an article in 2018, having experienced the challenges of growing an engineering team on top of a large, highly-coupled codebase, they went for this sort of Modular Monolith with stunning outcomes.
With a team of 25 engineers, 50,000+ lines of application code, and 100,000+ of test code, as he puts it:
We’ve been able to improve our application architecture and identify strong boundaries before fully extracting services. We can also make changes across services efficiently; it doesn’t take 5 PRs across 5 projects with deployment order dependencies to implement a feature. Our code is structured by domain concept, which especially helps new team members navigate and understand the project. The boundary between stateful and stateless logic helps us think about implementing some of our most complex business logic in pure Ruby, completely separated from Rails. We can leverage our dependency graph in interesting ways, including selectively running test suites for builds. Ultimately, whenever we do want to extract services that we can manage and run more independently, we’ll be well positioned to make it happen.
But an even more striking example is Shopify, the Canadian unicorn. As you probably already know, they are a global company offering a cloud-based e-commerce platform for small and medium-sized businesses.
They started operations in 2004 with a highly-coupled monolith. Yet, with time, it began to become unmaintainable, making it hard to deliver new features. That led them to approach the Domain-Driven monolithic style too.
In 2019, the company reported it had served more than 1,000,000 businesses in approximately 175 countries to date, with a total gross merchandise volume of over $61 billion that very same year. Not bad for a monolith, eh?
Their success has been so enormous that now they’re expanding this architecture to their other monoliths and preparing to apply it to their new apps by default.
It’s still a work in progress, but they are already experiencing excellent results. As Philip Müller, put it in the latest update on the topic from the company:
While we’re far from finished, we already reap the benefits of our work. The added constraints on how we write our code trigger deep software design discussions throughout the organization. We see a mindset shift across our developers with a stronger focus on modular design. When making a change, developers are now more aware of the consequences on the design and quality of the monolith as a whole. That means instead of degrading the design of existing code, new feature implementations now more often improve it. Parts of the codebase that received heavy refactoring in recent years are now easier to understand because their relationship with the rest of the system is clearer.
The only but? Starting from a big ball of mud made the challenge much more difficult for them, and still today, it’s hindering their ability to fully establish clear boundaries between components.
Don’t make it so hard for yourself.
Like a good chef, always keep your kitchen clean. Default to the Domain-Driven Monolith and embrace it early on. Things will come easier for you.
Still…no single size fits all.
I believe this is an adequate solution to cover most situations for makers.
But note, like everything, this is not a one size fits all glorious solution.
You might be crafting a short-lived prototype that might call for a simpler, more anarchic design. Or you could be building a critical system requiring a more complex one.
No context is equal, so you might find better, more appropriate answers for your particular scenario elsewhere. Remember, in the end, a system will only be majestic if it ultimately makes life better for you and your customers.
Get on the sustainable boat.
When you are a creator starting a new business, you don’t simply need an app. If you want to stay in the game for the long run, you need it to be majestic.
The Domain-Driven Monolith gets you closer to it.
It’s an easier system to understand and work with. It doesn’t put on your shoulders more than you should sustain.
And, on the off chance, it also offers you a gateway for a smooth transition into microservices/SOA if you really need it in the future.
That’s the coolest thing about it: you can have an application that sustainably grows with you and your business. A system that will not leave you hanging in the middle, nor will it make you invest in excess “just in case.”
However, that’s not granted yet. Till here, nothing impedes us from overcomplicating things and getting into many of the very same microservice issues we are trying to avoid in the first place.
How can we get around those traps?
Well, we’ll have to leave that for a future article.
In the meanwhile, slice up your app. Long live the Domain-Driven Monolith!
Say it out loud. Is there anything you disagree with? Anything missing that you’d like to add? If so, I’d love to hear your thoughts so, please, leave them in the comments.
Get on board. Do you want to receive more content like this right in your inbox? You can sign up for the newsletter here or in the form below 👇.
Share it. Do you think someone you know may enjoy this post too? If so, please forward it to them.
Have a creative time.
Leave a Reply