You’ve built your business on a large rock, it has served you well over the years, buuuut... technical debt has accumulated everywhere around your applications.
Your apps have performance issues or defects that are affecting your customers. Your developers are afraid to touch many areas of the monolith codebase to fix these problems. There is little test coverage and that makes it all the harder to invest in refactoring and stabilizing the system. It’s whack-a-mole every day with production issues. And if that wasn’t enough, your build times are long and releases are too slow. Even small changes have become difficult to release. You are likely looking at your monolith and saying, “If we could just make the changes we needed to make and not have to re-build, test, deploy, and certify all that other stuff we could move so much faster..”
You wish you could rewrite it all but unless you have the time and money to do it, that is likely out of the question.
You have heard about microservices and see a path out of your pain by breaking up the monolith to allow work to move faster with more discrete build and deploy pipelines.
If that sounds familiar then you have come to the right place, this article is intended to offer guidance on this journey.
Let’s clarify our terms
What’s a monolith?
“An application that is made up of one large codebase that includes all the application components, such as the frontend code, backend code, and configuration files.” –Roxanna Elliot
Examples: Classic J2EE, Ruby on Rails or .NET applications
What’s a microservice?
There are varying opinions on what a microservice is. Sometimes people use the terms “microservice” and “service” interchangeably. Those folks are not wrong but let's be a bit more specific. Here are some design considerations to help clarify what a microservice is.
According to Red Hat, a microservice is a service that adheres to these principles:
- Single concern
- Carries its own data
- Inherently ephemeral
In another post on Design IDEALs the author describes microservices that adhere to these principles.
- Interface segregation
- Deploy independently
- Availability over consistency
- Single responsibility
So then, true microservices are small and single-concerned, versus coarser grained services that include larger domains or sub-domains.
To help differentiate between them, I affectionately call those larger services "miniliths". Compared to the monolith they are still microservices but they don't match the strict definition of one. Makes sense?
Now that we have introduced some terms, let's address the folks that are really attracted to the “micro” part of microservices. The antidote to an old and bloated monolithic app is not hundreds of new “micro” ones. Costco does not sell enough Kleenex for all the tears you will have if you go too far down the "micro" rabbit hole. In order to succeed you need to find a balance.
A warning about microservices
The risks of having too many microservices are serious, so let’s talk about them.
Real risks of having too many microservices
- Too many moving parts for your staff to live with effectively and reason about.
- Poorly defined domains result in excessive dependencies on other services.
- Performance issues on critical features.
- Customer experience degraded in unexpected ways.
- Disrupted revenue streams.
- Higher cost of operation. (more builds, API’s, environments, apps, db’s, instances, runtimes, etc.)
- More complicated release testing.
So if it’s going to cost more to run, be harder to operate, and be more difficult to train on, then I think it is reasonable for us to consider the tradeoffs on the number, size, and costs of prospective microservices. Often a tiny microservice totally makes sense, and that is the right answer. I emphasize this point because it’s up to technology leaders like you and me to make the best design choices today for our teams tomorrow.
In the beginning, favor larger “services” and allow for coupling in domains and subdomains that are sensible. Having fewer moving parts will really help you deliver early wins and allow your team more time to form opinions on what refactoring is actually needed. Embrace the minilith! Yes, coupling is a good thing, at least to a point.
Don’t go full throttle into decoupling features into “micro” services unless they are simple no-brainers or you really must.
Where should we draw the lines?
Finding good cracks in the rock
fracture plane: A two-dimensional plane (or other surface) of weakness along which a crystal or other solid will break when under stress. --Wikipedia
My approach for finding places to break a monolith is largely inspired by a book called Team Topologies by Manuel Pais and Matthew Skelton. Definitely check out that book if you haven’t already.
Fracture planes first principles
The new bounded contexts you create should honor these principles:
- Value streams - Start with listing your current lines of business as well as strategic future ones. These are the best cadidates for new miniliths. For example, imagine that a property management system generates significant revenue by managing physical room-keys and that next year they'd like to add an insurance product to generate additional revenue. In that scenario, consider miniliths for unit-key-mgmt and unit-insurance.
- Teams and their cognitive abilities - A service should be owned by a single team and complex domains should be broken so that sub-domains are owned by a single team.
- Technology boundaries - e.g. mobile, desktop, web, peripherals, AI/ML, legacy domains, etc.
- Performance - Some services require high speed, low latency or large transactional scale and others simply don't. For example you might have an email service that is designed for sending 10M messages a day or you might have API's that require microsecond performance. Let features like those be their own services if being part of a minilith hinders its performance or scalability.
It might take you weeks to do this for a large monolith, but you really shouldn’t get too far into building the next-gen services without first mapping out all of the modules, high-level features or services that your monolith currently supports. Determining which modules depend on others will help to show which pieces can be separated or made common. You’ll likely need to dig into the code in places to determine just how tightly bound together certain pieces are.
I typically do this on a wiki page or a Figma canvas and create groupings of services and features so that the team can visualize what a feature is and where it fits best in the architecture..
The main goal of this step is to list your features and to start thinking about how they might be grouped together.
A hypothetical property management platform might have a list of features that looks like this:
Consolidate to the smallest list of services that you can live with
Now that you have a list of features, my guess is that you likely have a large number of new candidate services. Don't turn every feature into its own microservice! It’s time to reduce that list of services to the absolute smallest list that you can live with. Do that by creating miniliths that can cover a group of related features.
Following the example list of features above, consider a new minilith for building maintenance that looks like this:
And consider another minilith for buildings that includes these features:
Those two miniliths are likely a better choice than 16 new microservices! Some coupling is A-OK. Do it. Allow for some coupling as a way to reduce the list of candidate services into the smallest number of domains and subdomains that are still coherent.
This is your best shot at creating a plan that you can actually achieve. Being too ambitious at this step is likely unrealistic, sorry.
Stabilize first, then start changing things
Remember Red-Green-Refactor? You only want to start refactoring when things are green.
Don’t start blazing toward your new architecture without first stabilizing what you already have. Part of the reason you are in the position you are in is because your team hasn’t been investing in tidying and stabilizing what you have. Now is the right time to start building better habits.
Don’t undervalue making the thing you have work better for you. The cost of replacing an older application is huge and usually takes years. The urge to just get started on the newer and funner stack is very appealing, but this is the time for you to get real about how long it's going to take you to actually replace the old one.
Today is a great day to start practicing and encouraging the habits of refactoring, stabilizing and writing tests, etc; so that you and your team are fit for building the next thing. Start this as soon as possible if you haven’t already. Delivering quality is a lifestyle.
All too often the same bad habits in the team’s rigor will pollute the next-gen implementation. So check yourself on this when you’re getting ready to build. To say it another way, don’t bother starting something new if you don’t have a red-green-refactor mindset.
Do you need to implement some specific test scenarios in CI/CD before you start making changes? Would an end-to-end test help prove stability as you start changing things? Yes, just do it.
Prioritize the enablers
You likely have already identified a number of key refactorings or foundational new frameworks or enabling patterns that your next-gen approach will need. We want to make sure to get these enablers built early so that downstream delivery is unblocked and productive.
Some things to prioritize:
- Pin your test cases to make sure that things are still working when you start breaking things apart.
- New service needed? Put it at the top of the list.
- Create service design documents for use as a straw-man for the future state.
- Research frameworks and vendors for the fancy new tools you'll be using.
Let us help you
Lab Zero is here to help you rationalize, design and lead your hardest application modernization problems. We would love to help your technology get to a better place.
Continue the conversation.
Lab Zero is a San Francisco-based product team helping startups and Fortune 100 companies build flexible, modern, and secure solutions.