You’re writing more code than ever before. The trick is knowing what should be a microservice, and what shouldn’t.

These days, you can't swing a dry erase marker without hitting someone talking about microservices. Developers are studying Eric Evan’s prescient book Domain Driven Design. Teams are refactoring monolithic apps, looking for bounded contexts and defining a ubiquitous language. And while there have been countless articles, videos, and talks to help you convert to microservices, few have spent any appreciable time asking if a given application should be a microservice.

There are many good reasons to use a microservices architecture. But there are no free lunches. The positives of microservices come with added complexity. Teams should happily take on that complexity...provided the application in question benefits from the upside of microservices.

Please Microservice Responsibly

Matt Stine and I recently spent a few days with a client walking through some of their applications. The discussion started from a standpoint of "everything should be a microservice" as it often does these days. The conversation stalled as people argued over various implementation details.

That prompted Matt to write a set of principles on the whiteboard. These simple statements guided us the rest of the day. They led us to question every part of the application architecture, looking for places where a microservice would deliver value. The list fundamentally changed the tone of the conversation and helped the team make good architectural decisions.

To rid the world of surplus microservices, we present this list to help focus your efforts. Read through the following principles and ask if the application in question benefits from a given principle. If you answer “yes” for one or more of the following principles, the feature is a good candidate to be a microservice. If you answer “no” for every principle, you are likely introducing accidental complexity into your system.

1. Multiple Rates of Change

Do parts of your system need to evolve at different speeds or in different directions? Then separate them into microservices. This allows each component to have independent lifecycles.

In any system, some modules are hardly touched while others seem to change every iteration. To illustrate, let’s pick an example, say a monolithic e-commerce app for online retail.

Our Cart and Inventory functions might be largely untouched in daily development work. But we might be constantly experimenting with our Recommendation Engine. We also want to diligently improve our Search capability. Splitting those two modules into microservices would allow those respective teams to iterate at a faster pace allowing us to quickly deliver business value.

2. Independent Life Cycles

If a module needs to have a completely independent lifecycle (meaning the code commit to production flow), then it should be a microservice. It should have its own code repository, CI/CD pipeline, and so on.

Smaller scope makes it far easier to test a microservice. I remember one project with an 80 hour regression test suite! Needless to say, we didn’t execute a full regression test very often (even though we really wanted to.) A microservice approach supports fine-grained regression testing. This would have saved us countless hours. And we would have caught issues sooner.

Testing isn’t the only reason we might split out a microservice. In some cases, a business need may drive us to a microservice architecture. Let’s examine our Widget.io Monolith example.

Our business leadership might have identified a new opportunity - and speed to market is paramount. If we decided to add the desired new features to the monolith, it would take far too long. We couldn’t move at the pace the business requires.

But as a standalone microservice, Project X (shown below) can have its own deployment pipeline. This approach allows us to iterate quickly, and capitalize on the new business opportunity.

3. Independent Scalability

If the load or throughput characteristics of parts of the system are different, they may have different scaling requirements. The solution: separate these components out into independent microservices! This way, the services can scale at different rates.

Even a cursory review of a typical architecture will reveal different scaling requirements across modules. Let’s review our Widget.io Monolith through this lens.

Odds are, our Account Administration functionality isn’t stressed nearly as much as the Order Processing system. In the past, we’ve had to scale the entire monolith to support our most volatile component. This approach results in higher infrastructure costs, because we are forced to “over provision” for the worst case scenario of just a portion of our app.

If we refactor the Order Processing functionality to a microservice, we can scale up and down as needed. The result is something like this diagram:

4. Isolated Failure

Sometimes we want to insulate our app from a particular type of failure. For example, what happens when we have a dependency on an external service that does not meet our availability objectives? We might create a microservice to isolate that dependency from the rest of the system. From there, we can build appropriate failover mechanisms into that service.

Turning once again to our sample Widget.io Monolith, the Inventory functionality happens to interact with a legacy warehouse system, one with less than stellar uptime. We can protect our service level objective for availability by refactoring the Inventory module into a microservice. We might need to add some redundancy to account for the flakiness of the warehouse systems. We may also introduce some eventual consistency mechanisms, like a caching inventory in Redis. But for now, the shift to microservices mitigates against poor performance from an unreliable third-party dependency.