Moving away from the Monolith — Monoliths are… OK?
Kind of a strange title, right? I mean, we’re supposed to be moving away from monolithic architectures… so, are monoliths ever OK in…
Kind of a strange title, right? I mean, we’re supposed to be moving away from monolithic architectures… so, are monoliths ever OK in today’s modern, cloud-driven world?
You don’t often hear about successful efforts to decompose a set of microservices into a monolith. That’s not a typo. It’s a pretty famous write up on how Amazon moved from a distributed services based infrastructure to a monolith and reduced costs 90%. It led to a lot of controversy, about how microservices must be awful since Amazon, arguably the king of lambda architecture, couldn’t do it. In fact, the article described moving from a poorly optimized, wasteful architecture to one that was smarter about consuming resources — but was still microservice based, just leaning on some positive aspects of monolithic design.
Monoliths do have a lot going for them. And they’ve been around a long time, running a lot of things in the world. There’s a certain sense of permanence to the monolith — in no small part because they tend to be consistent, stable, and long-lived.
Those attributes are an offshoot of their design process. Monoliths take a lot of work to build. There’s immense intention that goes into their design. Building a monolithic system is usually a multi-year project, involves a lot of advance planning, and even more orchestration at the release end of the pipeline. It’s an “industrial era” line of thinking, often using waterfall principles that embrace heavy design, planning, and quality assurance disciplines.
It’s a lot of overhead too. Waterfall projects can easily consume just as many project management and oversight roles as they do hands-on development roles. But it’s not all bad. Successful outcomes have led to our global communications network, putting humans on the moon, and building spacecraft that are still phoning home nearly 50 years later.
Maybe the most important take away from Amazon’s case is that we need to be intentional in our design. Going all-in on distributed microservices, or the polar opposite, putting all your eggs in one monolithic basket, is likely not your best course of action. Over the past 50 years the software industry has learned a lot.
There’s a lot to draw from both approaches:
The intentionality of monolithic design is one of the big wins we can model. This means bringing design thinking to the creation process. We can build teams around a “product mindset” and use mature design processes like Domain Driven Design and Specification by Example.
Monoliths tend to be (relatively) easy to deploy. Most monoliths use a single language, a single database, and deploy a single (large) application. That’s pretty straightforward when compared to the plethora of options and modalities some cloud architectures offer— just take a look at how many different services AWS provides (and then start to worry about multi-cloud deployment). Embracing everything is probably a bad idea, so having a service catalog and being careful about what we put in our toolkit is important. Likewise, creating stereotypes and architectural templates is a great way to make sure every team isn’t re-inventing every wheel.
It may be counterintuitive, but monoliths can also accelerate the build cycle, especially when paired with modern agile thinking. Part of this stems from the (relatively) simplified architecture and deployment model; there are fewer moving parts, which often translates into a simplified development pipeline. It’s easier to create a standardized build path when there are few tools and fewer platforms to take into account.
Modern, microservice oriented thinking gives us a lot of options (and lessons) as well:
Isolation is generally a good thing — moving away from the “big ball of spaghetti code” toward independent services and clean APIs makes things easier to reason about. It also makes it easier to separate services into independently scalable components, which can translate to huge cost savings.
Microservice architecture also gives us a lot of levers to pull when it comes to responsiveness. Specifically, those nicely isolated services function well in the face of external failure (other services going down), which means we can still be responsive even when parts of the system fail. Monoliths on the other hand tend to encounter “all or nothing” failure.
And while agile microservice teams often rail about the “bloat” of huge monolithic projects, I’ve seen the same thing on both sides: A microservice project that goes off into the weeds. Extreme independence and lack of project planning can have a downside. All that independence can lead to losing sight of our core value stream. Teams start adding lots of exciting new features, experimenting with new approaches, and pushing their agile boundaries... Sometimes leading to its own form of chaos and bloat. Maybe structure and pre-planning isn’t always a bad thing.
Systems architecture is complicated. Combine that with the landscape monoliths and microservices offer, throw in a dizzying array of cloud services, and we’ve got a lot to think about. Weighing all the pros and cons, and prioritizing our core value proposition is hard, often confusing, and usually riddled with tricky, ambiguous decisions. Navigating ambiguity is critical when faced with prioritizing the agility of microservices versus the reliable simplicity of monoliths, and taking the right design path forward.
I’m working on two different projects right now. One is going to be a modern monolith, with a few distributed tricks. The other is a nearly pure distributed lambda architecture, with almost no server infrastructure. Both are what I’d describe as “fit for purpose.”
My “monolith” is a B2C Elixir application. While the architecture is running in the cloud, it’s server-based, does pretty much everything on the server (for example, server side rendering with PhoenixFramework), and clearly relies on having its own infrastructure. But it’s not a pure monolith: Elixir means I can easily scale horizontally when the time comes, using the underling Erlang actor model. And we’ll be using Kafka as an ingress for consumer events, which nicely isolates inbound event consumption from processing. It’s a nice mix of both worlds.
On the other hand, my lambda project is 100% distributed. Other than a Kafka server to persist our event stream, everything is serverless. Events are sucked up out of the event stream by Rust lambdas, web pages are rendered from React.js (in lambdas), and when nobody is browsing the web site and no events are streaming it, it all goes to sleep with barely a footprint in the cloud. It’s a great solution for something that needs scalable processing power, but generally isn’t actively doing anything throughout most of the day.
In both, we’re being very intentional. We’re leveraging Domain Driven Design and using Specification by Example to capture acceptance criteria. We turn those criteria into executable tests, and the only path to higher environments is through test batteries. Separate ephemeral environments are stood up for test and preview platforms (a little harder for the monolith but pretty darned easy with the lambda architecture). The CI/CD pipeline emphasizes “automate everything,” and pushes all configuration into Infrastructure as Code. The CI/CD is basically the same, giving us a lot of really good outcomes: Delivering a well tested, robust, fit for purpose system to production-ready state, fast.
It all comes down to being intentional. Challenging our assumptions, and not just asking “can we build this.” Taking time to design the right solution, and recognizing design is part of the work. We face this ambigity with every project: Should we slow down and design a fit for purpose solution, or should we just ship something fast? It seems like cranking out code would be quick and cheap, right? (The truth is when we jump right into building, we’ll probably make some costly mistakes.)
When it comes to choosing the best path forward, I love this quote from Andy Walker’s article:
As our experience grows our ability to navigate ambiguity should also but only if we’re deliberate about why we’re doing something and whether we should do it.
Both microservices and monoliths have their benefits, and both have their place. Sometimes the best thing we can do is step back and challenge our assumptions. Stop asking “can we do this” and start asking “what should we do” and “what shouldn’t we do.”