Macro problems with microservices
The microservice architectural style is, as Mr. Fowler defines it, an approach to develop a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API. These services are built around business capabilities and independently deployable by fully automated deployment machinery.
While the idea behind microservice architecture is fine (and not new, after all), it is often neglected how much complexity microservices bring in the development.
Insight in brief
- Microservice infrastructure sounds like a solution to many challenges of modern software development.
- However, we must be careful about it; developers have adopted it too readily, persuaded by the benefits.
- Before diving in, we have to understand all the implications that come with it.
Wait, complexity? Shouldn’t microservices reduce the complexity by separating software components? Yes. One of the goals of microservices is to reduce coupling between
application’s components. Each microservice has its own environment, lifecycle and processes. We can fine-tune one service, scale it up if needed, upgrade, etc., without having an impact on the rest of the application; this is something that wouldn’t be easily possible with monolith architectures.
However, this is just the happy path of the story. The dark side is where complexity strikes back and challenges your project’s decisions. I will try to summarize some (painful) points I experienced on real-life projects that were built using microservices architecture. To be completely fair, some of those projects were highly dynamic (in terms of the number of different components that varied during runtime), so you may not suffer from all the same issues – but at least you will learn about things that require more attention than you actually anticipated. Don’t say you haven’t been warned ;-)
1) Be ready to isolate – not just microservice’s code, but the whole development and release cycle. This includes the building of the code, tests, packaging of the artifacts and finally, releasing and promoting the microservice. Now you have to do this for every microservice in the application. This also requires a certain level of consistency applied across the component boundaries. Be prepared to invest some more time, usually during the beginning of a project (when isolation is probably not the very first thing you would worry about). If isolation is not done right, you might end in small hell of different component versions and strong dependencies among them, something you wanted to avoid in the first place. No one wants this.
2) Don’t forget who you’re talking to – often the calls to a remote microservices are wrapped to encapsulate the transportation, parsing/serialization to the domain objects, validation checks and so on. This makes usages of a remote API identical to usages of local code: just call a method, it does the ‘magic’ and returns the value. Visually, there is no difference between calling the remote or local method. And that is the breaking point!
After some time, your team might ‘lose the sense’ of which type of method is being used and start treating remote calls the same as local calls (e.g. invoking in a loop, synchronize, use in transactions…). But they are not the same. A remote call can fail anytime. It may be slower, with unexpected lags. You should not send everything over the wire, due to security. The order of execution of remote calls is unpredictable. There is no rollback. A remote call can even return garbled data. Hence every time when you call the remote method, you must be aware of that.
3) Do not assume that remote call just works. In fact, force yourself to think what would happen if a remote call failed and how you would handle all possible failures. The code might start appearing to be full of the ‘noise’; however , that is not the reason not to do it, but to architecture the code better. An application I was once working on, got stuck in an unexpected inconsistent state, where a few remote calls failed in the middle of some business logic. There was no code for handling
transportation errors, neither code for rolling back the current work. Since application could not recover from this weird state, everything stopped. In production. On day two. Sigh.
4) Local development is no more local – unless you have isolated development teams per microservice, good chance is that developers will need other microservices running locally (dependencies), so to be able to develop their own module. That additionally complicates everyday development. I strongly believe that a developer should focus on his own work and not waste time setting up the project or fidgeting daily with the infrastructure.
Be prepared to address many challenges here – from running the minimal number of infrastructure components locally, setting the DNS names, enabling debugging and profiling, up to making mockups for some remote calls. Docker will become your best and worst friend. Be ready to invest in development tooling, shared libraries, internal artifact registries, proper versioning of
artifacts… Give your developers the freedom to code without much hassle. Happy developer produces great code.
5) Document APIs and communicate – like the world depends on it! Learn how to build, expose and maintain good APIs and contracts. When API changes, it breaks other components; in the microservices realm this happens during the runtime, so don’t wait for integration tests to detect the changes. Be sure to propagate changes among teams that develops depending services. Explain your API (preferably using some common format like Swagger), add examples on how it is designed to be used. Don’t let developers wonder what your API is supposed to do. While this is important for any architecture, with microservices it becomes super-important. Some significant amount of time can be wasted on just figuring how to use a remote API. Moreover, maintain and communicate dependency structure: work on specifying upstream/ downstream/compile-time/runtime dependencies for each service.
6) Integration tests are a must. What would be a unit test with mockups in monolith architecture, with microservices it becomes an integration test. Now you need to test the real interactions between the microservices. The number of integration tests may easily explode beyond the level of usability – when running all tests takes the crazy long amount of time, even for the still small code base, making CI painfully unusable. However, I would like to address here something else: not everything has to be in an integration test. Separate concepts that can be mocked and tested with unit tests from those that require calling the remote service. This approach can significantly reduce a number of integration tests! Be prepared to invest time in building solid mockups and still having enough of unit tests. Try to reduce dependency on infrastructure and remote services.
7) Stress the application and the infrastructure as soon as possible, so you don’t get stressed later. When a number of software components that communicate over the network rises, there is no other way to figure how the application behaves other than to stress it. Doing this in an early phase will give a solid feedback on where the weak points are. Use stress as the measurement of optimizations and as the tool to determine the thresholds of overall application performances. Use stress also to break your application by running it over the edge and check if the application can recover from the stress. I can’t stress enough how much stress tests are important for the application built with microservices.
8) Not everything is a microservice. It can be very painful (and expensive) if you design a software component as microservice, while it really is not. Be aware of differences between microservices and modules and libraries. Always question your decision; for example, if you start calling remote microservice frequently because it provides important functionality for your component, that microservice may be a good candidate to become a library. Organize microservice around business features than around micro set of functionalities. This is one of the conceptual things and people may see differently where the boundaries are; build definitions and practices that are applied in your application and among microservice components.
9) Development takes more time, obviously. How much more? It depends on the complexity of the project, number of components and the amount of communication between the components; and whether you have addressed all these points. Double your predictions until you have all sorted out.
As you can see, developing microservices requires more attention than one could assume. But that is only one half of all issues. In the next part of this blog series, we will talk about infrastructure challenges in microservice architecture.