Hi friends 👋 Have you heard about modular monoliths? It's the hottest new trend in software development these days. Everyone is talking about them. You can't go to a coffee shop without overhearing people saying "I've been vibe-monolithing all week."
Ok, that was a lie. There's nothing new about modular monoliths, but I do think it's a topic worth talking about. Also, it's hard to write about anything that is not AI these days, so I hope you forgive me for putting on my best car salesman face today to tell you why I think they're so great.
This week we also have a brand new episode of the Frontend.fm podcast, a great talk about API design, a bunch of amazing blog posts, and much more.
Let's dive in.
MAXI'S RAMBLINGS
May I Interest You In a Modular Monolith?
Is it just me, or… are monoliths cool again?
Alright, maybe cool is a stretch. But at least they’re not as uncool as they were a few years ago, back when microservices were the hot new thing.
Now that developers have come to realize that the benefits of distributed architectures don’t come for free, monoliths seem to be making a comeback. The difference is that now, the monoliths are “modular.”
But… what does that mean exactly? How is a modular monolith different from an “un-modular” monolith? Is there a modularity add-on we can purchase? If so, is it a one-time purchase or a subscription kind of thing? It doesn't have ads, does it?
Great questions, and we'll get to all of them. But before, let’s talk about why you’d want a modular monolith in the first place.
The Making of a Big Ball of Mud
A common reason people advocate for micro-services or micro-frontends is that the monolith they’re dealing with has become too disorganized.
Things probably started well in the beginning—the team was moving fast without breaking things, shipping features left at right on a daily basis, and, more importantly, keeping customers happy. As the cool kids say these days, the team was cooking.
But every monolith has its limits. Over time, the team started complaining about builds taking forever, people stepping into each other’s toes, and how they couldn’t change any part of the system without affecting everything else.
All of these are signs that the well-intentioned monolith has turned into the infamous big ball of mud.
When this happens, someone on the team inevitably brings up the idea of decomposing the monolith. “Enough is enough!” your teammate says, as they start plotting a strategy to adopt micro-frontends, micro-services, or even micro-breweries (but only if they are from Southern California.)
And it’s hard to blame them, really. When you’re dealing with the challenges of a messy monolith on a daily basis, the idea of micro-anything sounds very appealing.
"We can build and test one thing at a time instead of the entire project? We can deploy one piece of the app without affecting anything else? Yes, please! Where do we sign up?!"
But here’s the thing: when your starting point is a big ball of mud, migrating it to a distributed architecture won’t give you the nicely decoupled codebase you’re thinking of—it will give you a distributed big ball of mud, which is arguably much worse.
This is where modular monoliths come in. They let us get some of the same benefits of micro-services or micro-frontends (i.e. decoupled and cohesive modules), without the drawbacks of distributing our application across the network.
The million-dollar question of course is: What makes a monolith modular? Is there a secret recipe we can follow to inject some modularity into our messy monoliths?
Well, yes, there is! And the good news is that you only need two things: organization and encapsulation.
Organization vs Encapsulation
Organization is about how your monolith is structured. By this, I mean both your folder structure and your logical structure, which is the criteria you use to break down your monolith into smaller chunks.
Layered Architecture, Clean Architecture, Hexagonal Architecture, Domain-Driven Design, and a dozen others are all tried and tested ways to organize an application. All of these have been heavily documented over the years, and the reason they’re so popular is that they actually work.
But choosing a way to organize the codebase is only half of the equation. You might follow Clean Architecture to a tee on your codebase and still end up with a big ball of mud, and that’s usually because you missed the other ingredient—encapsulation.
You can think of encapsulation as the degree of independence of your modules. In an ideal world, your modules would be truly independent and would never need to know about each other. But, of course, that’s not the case in real applications—and encapsulation is what helps us manage those dependencies.
To achieve encapsulation, you need to focus on two things: information hiding, which means preventing the implementation details of your modules from leaking out, and guardrails, which set the rules for how your modules are allowed to talk to one another.
Let’s talk through these with an example.
Imagine we’re building some sort of invoicing app, and we decided to be good architects and keep our modules nicely organized by domain. So we have an Invoice module, a Customer module, a Product module, and so on.
Now, let’s say the Invoice module needs to print some customer information on the page, like their name, address, and favorite ice cream flavor. The problem is that this data comes from the Customer module, so how can we get it?
One option, and probably the most convenient one, is to import the “store” of the Customer module directly. After all, there’s nothing stopping us from doing so. (For this example, feel free to think of the “store” in any way you’d like—as a Redux store, a Backbone Model, or just as a function that calls an API.)
This might not look pretty, but it works. And by itself, importing another’s module store directly isn’t likely to cause too much harm to the overall health of the system.
But imagine this happening dozens or hundreds of times. Imagine what happens when every module in the application can reach into the internals of another module and borrow any piece of code to use it as its own.
I’m sure you won’t need to do too much imagining here, because you likely experienced this happening on your own codebases. You know that it doesn’t take long for things to get out of hand and end up with a tangled mess where everything depends on everything else.
So let’s rewind a little bit and see how this would look like if we focused on encapsulation:
First, let's look at information hiding. The store of the Customer module is an implementation detail, so the code outside of this module shouldn’t know anything about it. Instead, we could have the Customer module expose an interface that lets other modules communicate with it via a predefined contract.
The interface in this case could be as simple as a getCustomerInformation()
function that the Invoice module could use to get the data it needs. It’s a subtle difference, but it can have a big impact. The Customer store could be re-implemented in Rust for all the other modules care about—as long as it sticks to the contract, all the other modules will be happy.
Next, we need to set up some guardrails to actually protect this contract. Ideally, we would have some sort of access modifiers in JavaScript to mark certain files or packages as “private” or “protected,”… and maybe one day we will. But until then, we need to get a bit more creative.
Thankfully, there are a number of tools that can help us define guardrails in our project, including ESLint and, my favorite, Dependency Cruiser. With these tools, we could specify that we don't want to allow modules to import files from an adjacent module, with the exception of the module's interface (which, again, could be as simple as an index.js file at the root of your module.)
I wrote about these tools before, so I won’t bore you with the details here. If you want to learn more about them, check out issue 36.
These tiny improvements might seem insignificant at a lower scale, and none of them will turn a messy monolith into a modular one overnight. But they add up, and they're our best defense against letting entropy take over our codebase.
You might be thinking that modular monoliths sound too good to be true. We get all of the benefits of a distributed architecture with none of the drawbacks? Where’s the catch?
The catch is that modularity is not easy.
It is simple (organization + encapsulation = modularity), but applying it consistently requires discipline and hard work.
You might also find that a distributed architecture like micro-services or micro-frontends is the right fit for your project and your team. After all, there are a lot of legitimate reasons why your project could benefit from being independently deployable.
But no matter which path you choose, it’s important to invest in modularity within the monolith first—even if it’s just as a stepping stone. Otherwise, you’ll risk turning your monolith into a distributed big ball of mud, and well, that just sounds like bad times for everyone.
FRONTEND.FM
Remote Browsers and the Feature Phone Market with Tom Barrasso
In the latest episode of Frontend.fm I chat with Tom Barasso, Head of Developer Relations for Cloud Phone at CloudMosa. We explore the world of feature phones, how remote browsers make it possible to run modern apps on these low-end devices, and the unique challenges and opportunities for developers in this space.
I learned a lot about the feature phone market in this conversation with Tom—and I'm sure you will too!
You can listen to the full episode on YouTube, Spotify, and Apple Podcasts. Don't forget to subscribe to the podcast on your favorite platform to be the first to know when a new episode comes out.
Add to your queue |
ARCHITECTURE SNACKS
Links Worth Checking Out
- Dominik Dorfmeister gave a talk at React Paris last month, sharing everything he learned about API design from his years maintaining the popular React Query library. Lots of great advice on this one—not just for open-source maintainers but for anyone writing code that is meant to be used by someone else (which is, like, most of us.) I highly recommend giving this one a watch.
- I really enjoyed this conversation between Gergely Orosz and John Ousterhout on The Pragmatic Engineer podcast. I'm a big fan of Professor Ousterhout's philosophy of software design, and it was great to hear his thoughts on the role of design and architecture in the AI era.
- Speaking of AI, Google released a 69-page paper on Prompt Engineering, full of tips and practical advice on how to write better prompts. Surprisingly, asking LLMs to "pretty please return valid JSON" is not a super effective strategy.
- Matthias Endler wrote a great essay on the traits of the best programmers he knows.
- As developers gain more experience working with AI-powered IDEs like Cursor, they're documenting and sharing their experiences on what works, what doesn't, and what kind of works some of the time—and I'm here for it! The latest example is this post from Manuel Kießling, which has some really good advice for senior engineers.
- I enjoyed this article from Ben Northrop on how to build a long, sustainable career in tech without burning out: Always do Extra.
- Josh Collinsworth wrote a wonderful article on the blissful zen of a good side project.
- Have you read enough blog posts about how React Server Components work already? That was a trick question; you could never read enough blog posts about how React Server Components work. Luckily for us, Dan Abramov gots us covered with a super-long one.
- And if that wasn't enough React content for one day, here's a deep dive by Christian Ekrem on how React's reconciliation process works under the hood.
- Sean Goedecke is back with some hard but important truths. This time, telling us why it's key to know where our salary comes from and why we should do our best to connect our work to profit.
That’s all for today, friends! Thank you for making it all the way to the end. If you enjoyed the newsletter, it would mean the world to me if you’d share it with your friends and coworkers. (And if you didn't enjoy it, why not share it with an enemy?)
Did someone forward this to you? First of all, tell them how awesome they are, and then consider subscribing to the newsletter to get the next issue right in your inbox.
I read and reply to all of your comments. Feel free to reach out on Twitter, LinkedIn, or reply to this email directly with any feedback or questions.
Have a great week 👋
– Maxi