Getting off the Microservices bandwagon
Start thinking in domain models and service boundaries again
Netflix uses Microservices. They deploy countless times a day without service interuptions. We should all do Microservices. Really?
Microservices, designed well, increase fault tolerance and minimize the footprint any given release to production can have. What Microservices do is isolate development and allow us to test against a constant size of code as we prepare a production release.
Monoliths on the other hand grow over time. Regression tests become larger and larger. Yelp for example had to build a custom distributed architecture so that their test coverage could keep up with development.
Focus on the goal — Release often, release fast
What we want is not microservices but an efficient way of releasing at a constant rate. Simply put, a traditional monolith will become more complicated as features are added. With each new feature development and QA will take more time. Lets say that for each new feature development effort for an additional feature will be 1.25x the effort. With each new feature QA will be 1.5x the effort. This is obviously assuming that the complexity of the feature itself will be constant.
This never actually happens like this. Teams adjust, they hire more team members, they try to find something to improve their workflow, teams themselves work faster as they understand the product more etc.
But the key concern still exists: a feature’s development time is a function not only of its inherrent complexity but also of the size of the codebase into which it is introduced.
Microservices change this by keepting the codesize into which a feature is introduced constant. Instead of adding a new feature into an ever large pile of code, we build a new seperate service.
Complexity is a cost
Microservices allow us to develop in constant time, ensuring that we can deliver feature after feature without being overwhelmed by all the work we have already done. Our code can stay fresh, young, and vibrant.
But we have other concerns: tracability, monitoring, debugging. If a single inbound request will hit 2 or more services then tracing the request becomes a bigger challenge; consistent logging, helping us to investigate what happened, becomes a challenge; debugging a microservice architecture becomes a challenge; state management and caching becomes a challenge.
There are some answers: debugging shouldn’t be a challenge. If the applications are well written, functionally compact, and follow a service contract we are ok…
Anyone who has ever written code knows that timelines, cost concerns, priorities etc. make the above very difficult to achieve in practice. As our service contracts expand we need to look at how to maintain backward compatability — do we run two services v1 and v2 in tandem? Now monitoring and routing becomes a challenge as existing consumers probably did not specify a version. Feature creep can be a problem — most changes start out as a small addition to an existing functionality. Too often they are bloated and have their tentacles deeply embedded into the code before we notice this new functionality was actually a foreign invader.
But these are solvable problems for large teams with developers dedicated specifically to a service. But if you have just 20 developers developing 15 microservices to deliver an application this might not be a good strategy. The maintenance burden of orchestrating 15 microservices will be so large that it will probably absorb all the gains from reducing development time.
Focus on objectives, strike a balance based on practical realities
Let’s say you want to build a set of services that expose the core functionality of your business. As you do a domain exploration you discover you will have 50+ services.
Your first step should not be to chase that Microservices utopia. Look at how these services are composed and how suceptible they are to changes in the ecosystem. The ecosystem here is all the space that you must interact with but cannot control.
Second, look at your team composition. How big is your team. If you have 10 developers 50 Microservices with their individual development pipelines, repos, etc. may not be sustainable.
My suggestion: group services into parent groups, and into release velocity groups to discover the practical ideal for service granularity.
Some services may justify individual domains but may also be grouped together — we may discover a parent domain that they all relate to. It may be worth considering publishing the parent domain as a single service to reduce orchestration and maintenance burden.
This one is harder to guage but generally speaking services that power UI layers (highly dynamic ecosystem) will need to be updated and changed more frequently than services that deal with backend core systems (near static ecosystem). When we are looking at services where we control the upstream and downstream pieces fairly completely we can justify building larger services that have longer release cycles. Services that need to be nimble to enable the latest user capabilites may require to stay small so as to embrance the next trend quickly.
These two ways of grouping our domains should help us understand our service granularity. What we will end up is some set of services that act almost like small monoliths, many small to medium sized services, and a few microservices that trully share nothing.
When approaching architecture we should not focus on catch phrases and the most current trend; we should also not adopt best practices that work for Netflix. What is necessary for an organization serving 125 million users may be an impediment to an organization trying to capture its first million. Always focus on first principles and design systems that make sense in the context of the product, the organization, and the consumer.