This is part of a series of blog posts exploring various design factors in microservices. Here are links to more in-depth looks at the individual topics introduced in this post:
Part 2: Multiple Rates of Change
Part 3: Independent Life Cycles
Part 4: Independent Scalability
Part 5: Failure Isolation
Part 6: Simplify External Dependencies
Part 7: The Freedom to Choose the Right Tech for the Job
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 amount of 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, and 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
A few years ago, my colleague Matt Stine and I 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? If so, 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.
Read more about multiple rates of change.
2. Independent lifecycles
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.
A 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.
Let's say our business leadership has identified a new opportunity and speed to market is paramount. If we decide 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.
Read more about independent lifecycles.
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 good that 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:
Read more about independent scalability.
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.
Read more about failure isolation.
5. Simplify interactions with external dependencies (a.k.a. the façade pattern)
This principle is similar to “Isolated failure.” The twist: we focus more on protecting our systems from external dependencies that change frequently. (This could also be a vendor dependency, where one service provider is swapped for another, such as a change in who does payment processing).
Microservices can act as a layer of indirection to insulate you from a third-party dependency. Rather than directly calling the dependency, we can instead place an abstraction layer (that we control) in between the core app and the dependency. Further, we can build this layer so it’s easy for our application to consume, hiding the complexity of the dependency. If things change in the future—and you have to migrate—your changes are just limited to the façade, rather than having to do a larger refactoring.
Read more about simplifying interactions with external dependencies.
6. The freedom to choose the right tech for the job
With microservices, teams are free to use their preferred technology stacks. In some cases, a business requirement suits a specific technology choice. Other times, it’s driven by developer preference and familiarity.
NOTE: This principle is not a license to use every technology under the sun! Provide guidance to your teams on technology choice. Too many disparate stacks add cognitive overhead and can be worse than standardizing on a “one size fits all” model. Keeping third-party libraries current for one stack is challenging enough. Multiply that toil by four or five, and you’ve signed up for quite an organizational burden. Do what works, and focus on “paved paths” you know how to support.
In our Widget.io example, the Search functionality might benefit from a different language or database choice than the rest of our modules. It’s straightforward to do this if desired. And sure enough, we’ve already refactored it for other reasons!
Read more about the freedom to choose the right tech for the job.
That’s the technology discussion. Now what about culture?
No technical decision exists in a vacuum. So before you dive into the wonderful world of microservices, take a look at your organization. Does your org structure easily support a microservices architecture? What does Conway’s Law say about your chances of success?
Fifty years ago, Mel Conway theorized that a system designed by any organization will create a system that mirrors its organization chart. In other words, if your teams are not organized as small, autonomous groups, your engineers are unlikely to create software composed of small, autonomous services. This realization has prompted the Inverse Conway Maneuver. This encourages teams to change their org chart to reflect the architecture they wish to see in their applications.
You should also consider your cultural readiness. Microservices encourage small, frequent changes, a cadence that often conflicts with a traditional quarterly release cycle. With microservices, you won’t have code freezes, or a “big bang” code integration. While a microservices-based architecture can certainly function in a traditional waterfall environment, you won’t see the full benefits.
Consider how you provision infrastructure as well. Teams that focus on self-service and optimizing the value stream often adopt a microservices paradigm. Modern platforms like Tanzu Application Service help your teams quickly deploy services, test, and refine in a matter of minutes rather than weeks (or months). Developers can spin up an instance at the push of a button, a practice that fosters experimentation and learning. Buildpacks automate dependency management. That means developers and operators can focus on delivering business value.
Yes, even your company in your industry can move away from four deploys a year to, well thousands a month. #springone pic.twitter.com/Dqpr7qHDfX— Nate Schutta (@ntschutta) December 5, 2017
Finally, let’s ask two specific questions of the application:
Does this application have multiple business owners? If a system has multiple independent, autonomous business owners, then it has two distinct sources of change. Conflict can arise from this situation. With microservices, you can achieve “independent lifecycles” and please these different constituencies.
Is this application owned by multiple teams? The “coordination cost” for multiple teams working on a single system can be high. Instead, define APIs for them. From there, each team can build an independent microservice using Spring Cloud Contract or Pact for consumer-driven contracts testing.
Answering yes to either of these questions should lead you toward a microservice solution.
The road to microservices is paved with good intentions. But more than a few teams are jumping on the bandwagon without analyzing their needs first. Microservices are powerful, and they should absolutely be in your toolbox! Just make sure you consider the tradeoffs. There’s no substitute for understanding the business drivers of your applications; this is essential to determining the proper architectural approach.
Ready to experiment with microservices today? Check out our guides and learning paths on the Tanzu Developer Center.
Want to learn more about microservices? Join us at the next SpringOne!
Need more architectural guidance for your modern apps? Be sure to download Nathaniel’s eBooks Thinking Architecturally and Responsible Microservices.
Editor's note: This was originally published in 2018 and has been updated to maintain accuracy.
About the AuthorMore Content by Nathaniel Schutta