Macroservices
Microservices have been the major architectural trend in recent years. In Gartner’s Hypecycle 2020, Microservices are “sliding into the truth”. Microapps are on the rise and more popular than ever. When it comes to application architecture it seems clear: smaller is better.
Although, since the term Macroservice has been circulating in social media, considering larger units comes in vogue again.
Smaller is better. But wait… is it?
Microservices, nanoservices, serverless, big ball of mud — or is it Macroservices? Well what now, small or large? At least one thing can be said with certainty: size seems to be decisive. Heuristics such as Amazon’s two pizza teams aim precisely in this direction.
Let us zoom out a little of this picture. No matter how big the service is, it is part of a system of services. Many services interact with one another, perform tasks collectively, serve a larger purpose. Therefore, they need connections and dependencies to one another. They form a system. That is why architectural design has always been system design.
And with systems it is inherently always about delimitation: what is inside, what is outside? The size of a service results from this delimitation. The decisive question is what moves within the limits, what is in between and what is outside.
The answer to that question is always a tradeoff. The larger the services, the fewer relationships or connections are needed between them. And the smaller the services, the more connections are needed.
Reliability
Fewer large services result in fewer relationships. Several smaller services result in more relationships. And these connections create potential problem spots. Why?
Think about services as a static set of interacting features for a moment. Interactions can be expressed as relationships between features. Whether we build few large services or several small services, the number of inter-feature relationships is stable, because their inter-dependencies stay stable.
However, when we divide services into smaller pieces, we generally tend to replace reliable connections with more unreliable connections. While in-memory communication can be seen as quite reliable, communication over networks may not. Networks do not always work reliably. Simply think of packet loss on the wire. In addition, we gain independent runtime, deployment and development life cycles of the individual services. This may lead to services becoming unavailable or incompatible throughout their lifecycle.
Complexity
While this may have benefits, the downside is that development, operation, and reasoning become more complex. To mitigate unreliability, a lot of work needs to be carried out to make such systems reliable and resilient again. Introducing tools such as service meshes, or implementing compensation strategies to mitigate unreliability make our systems technically more complex. Not to mention the cognitive (over)load that comes with it. The more unreliable relationships that need to be maintained between services, the greater the complexity of the system. Key is to find good balance between the complexity of the individual parts and that of the overall system.
Organizational development
The misinterpretation of the often-postulated team autonomy also increases the complexity of systems to some part. Autonomy certainly is a key factor if teams are expected to work efficiently and effectively. Nonetheless, we build systems! We must think in terms of systems.
By definition, a service or team without dependencies with others cannot be part of a system. Independent services are an illusion. Same is true for fully autonomous teams. There are no independent services/teams; there is always a relationship or dependency. Therefore, the goal cannot be to build autonomous services or set up fully autonomous teams, but to limit their (inter)dependencies or at least replace blocking ones with non-blocking ones.
Hence, software architecture also always means organizational development! As the saying goes:
If you did not design the organization, you did not design the architecture.
Socio-technical aspects of the system need serious consideration. To stay with the Amazon example: how big are the two pizzas? Mini pizzas, maxi pizzas or family pizzas? How many teams have ordered pizza? Do we need to coordinate delivery? Do we want to share or maybe order salat?
Teams, too, are limited in the amount of inherent, business, technical, and organizational complexity that they can handle. Team cognitive load is a limiting factor that must not be underestimated.
Heuristics
Patterns and heuristics, such as Bounded Contexts, from Domain Driven Design, independent service heuristics, or team-sized software, may help here. However, cutting services is and always will be an individual approach towards the domain or business in focus. It is not easy to achieve — it is hard work. That is why it is often tempting to prefer simple heuristics such as “small size”, but in the end they do not lead to desired outcome.
Good services design is more than considering size but more about complexity and boundaries — at business, organizational, and technical level. We need to shift focus to sensible delimitation that creates balance between complexity within single services and complexity in the overall system. Aligning services not only to technical aspects, but foremost to business capabilities, user journeys, and organizational processes is key — combined with high-alignment and resilience.
An extended version of this article is available in German at informatik-aktuell.de