In my previous post about software architecture I introduced a methodology for creating architecture for new software projects that aims to encapsulate areas of volatility in discrete services.
Going through this process is pretty straightforward for brand new projects, but who has the luxury of working on brand new projects all the time? Not most software developers!
The majority of software developers are primarily maintaining or adding features to existing software systems, but if you ask a group of software developers if they’d rather work on a brand new project or maintain an existing system, the response will overwhelmingly favor working on new projects. In fact, desire to work with newer technologies or on new projects is one of the more common reasons people cite for leaving a job. Why do you think this is?
When you maintain an existing system, it’s easy to identify problems with its architecture. Why? Because good architecture results in a system that’s easy to change.
When you’re asked to change an existing system that wasn’t designed to encapsulate volatility, the weakness of the design becomes self-evident. Even the smallest surface-level changes can send ripples of complexity throughout the rest of the system: new dependencies perpetuate seemingly endlessly, already bloated method signatures gain even more parameters, automated tests explode in size and complexity while reducing in actual utility.
Do these problems sound familiar?
Does your architecture look like this?
If so, you have a monolith.
The monolith is easily the most common architecture, or lack thereof, seen in the kinds of existing systems most software developers work on.
The monolith emerges from systems that weren’t designed at all, or weren’t designed to encapsulate volatility, but have grown in size and complexity as business needs evolve over time.
Everybody feels the pain when working on a monolith, developers and business people alike. Developers hate working on monoliths because the difficulty of implementing new behavior grows with each new behavior added to the monolith. Once a monolith reaches critical mass, changing the behavior of a monolith becomes a truly terrifying proposition. Productivity drops, morale drops, and the business is impacted. Businesses want to move fast, and monoliths become slower over time. This is not a good place to be.
Many developers will want to throw the whole thing out and start over, but this idea won’t fly with the business. You’ll hear something like this: The best software is the one that’s currently making you money, no matter how great the hypothetical new version is imagined to be!
So you need a new plan that doesn’t involve throwing out the monolith, but still allows you to move forward without further increasing the size of the already bloated monolith.
Microservices is a buzz word that vaguely conveys the idea of a service oriented architecture with many small discrete services.
On the surface, microservices seem like the antithesis of the monolith (and everyone hates monoliths!), so many microservice architectures emerge from monoliths by taking new feature requests and implementing them as discrete services, and herein lies the problem: when your monolith already encapsulates all of your system’s volatility, breaking out new features into discrete services doesn’t save you from any of that volatility!
At first, it might seem like implementing new features as discrete services is helping things.
Here’s what a monolith with a single feature implemented as a microservice might look like
The monolith is a little bit smaller, and the new feature is cleanly decoupled from the rest of the system. This is good right?
What happens when the next feature request comes in?
Uh oh. Two features in and we’re already seeing a problem. The second feature actually built on top of the first feature, so it can’t be completely isolated.
Let’s fast forward. Here is what you might end up with many features requests later if you simply carve out new microservices as feature requests come in without encapsulating complete areas of volatility
Let’s look at our microservices next to our original monolith
If you squint your eyes and let all those service dependency lines blur together, you’ll see the problem.
Small changes still send ripples of complexity throughout the entire system, they just do it differently now. Changing behavior of the system as a whole is going to now involve changing multiple microservices. All the same problems are emerging again, possibly even worse, exacerbated by the fact that the dependencies are harder to trace and carefully orchestrated multi-service deployments are even scarier than deploying a monolith.
The problem stems from the fact that microservices alone isn’t architecture. Microservices is simply a word that vaguely describes systems that operate as discrete services rather than as a monolith. The actual practice of software architecture involves carefully planning where to draw the boundaries between discrete services so that they encapsulate areas of volatility. When you simply take a monolith and implement new behavior as discrete services, you aren’t going through the necessary planning to think about areas of volatility in the scope of the full system.
So if monoliths are too big, and feature-driven microservices are too small, how do we get to just right?
Go through the process of designing an ideal architecture for your system as a whole as if you were going to create it from scratch.
You’ll wind up something that looks more like this
Obviously you can’t go directly from a monolith to this architecture all at once, but you’ll never get here if you don’t know where you want to go when you first start!
I’d love to give general advice for all software developers looking to break apart an existing monolith, but the reality is that this road map is going to completely unique to each system. The important points to takeaway from this post are