Services: From micro to nano and back
The differences, advantages and meaning of micro- and nanoservices.
Microservices are a popular software architecture these days, including at Tallence. Their advantages speak for themselves: they scale well, provide a modular structure, are error-resistant, easy to understand and quick to develop. This is achieved by dividing the overall system into the smallest possible ("micro") loosely coupled and independent backend applications ("services"), which mostly communicate via REST-interfaces.
A small mail order business serves as an example. Instead of an online store, orders are placed by telephone. Salespeople access an internal product catalog and enter orders into an internal ordering system. A shipping department takes care of packing and shipping the products.
The architecture consists of the following microservices:
- ProductCatalogService: Manages which products are available and how many are in stock.
- CustomerService: Contains customer data such as phone number, address, interests.
- OrderService: Manages orders and their state (in preparation, shipped, ...). An order references products from the ProductCatalogService and customers from the CustomerService.
- SalesCampaignService: For discounts, combo offers and other. References products from the ProductCatalogService.
The services need to work together in places. For example: When products have been pulled from the warehouse to be packaged, as an overall transaction, the inventory in the ProductCatalogService needs to be decreased and the order status in the OrderService needs to be set to "in preparation".
Another point of view
Looking at it from a different angle, weaknesses become apparent. In the architecture above, microservices reference data from other services, but they should be loosely coupled. The distributed transaction (packaging) can only be completed with the participation of multiple services. It is complex and tightly couples services.
Is this still in the spirit of microservices? To assess this, let's look at how their characteristics come about.
A step back to basics
When considering how the properties of microservices come about, it is helpful to look at the concept of bounded context.1
A bounded context is a technical context in which terms have a specific meaning and certain concepts and rules apply. Such contexts always exist, even if they are not consciously or explicitly defined. Many misunderstandings (and bugs) are based on using the same terms but unconsciously not talking about the same thing.
The mail order business from the introduction, for example, has the two bounded contexts of sales and shipping. The term customer is a good candidate for misunderstanding. It exists in both contexts, but with different meanings: sales sees a customer as someone who can be reached for quotes and advice via email address and phone number. In shipping, on the other hand, a customer is someone with a shipping address who has ordered products.
On the other hand, terms like discount or package size and rules like "20% off everything except pet food" and packaging guidelines belong to only one of the two contexts at a time.
Bounded contexts communicate with each other through events. Two examples are: An order was completed. A package of goods has been shipped.
Good context boundaries and events are immensely important. Then, bounded contexts are self-contained, loosely coupled to each other, and strongly internally coherent.
From Bounded Contexts to Microservices
To relate microservices and bounded contexts, the following approach helps: "Micro" means a service relates to exactly one bounded context. "Service" means "business services" rather than technical ones such as CRUD operations.2 A bounded context is the model, a microservice its implementation as an application.
For the application to be as self-contained as the model, it includes everything. Front-end, back-end, data storage, all resources and all data.3 From this point of view, for example, a database alone is not a microservice. It also follows that synchronous invocations, and REST interfaces in particular, do not make sense between microservices. A synchronous call would mean one service is waiting for confirmation or data from another. REST (and similar technologies, such as GraphQL) models data. Both of these conflict with the application owning everything it needs.
A microservice is therefore a complete application that provides all the business functions required for exactly one bounded context. According to this terminology, a monolith is an application that covers several bounded contexts. This explains typical problems with monoliths. For example, that a term has different meanings in different contexts, but must be reconciled in the implementation. This makes it complex and changes have to be coordinated with many stakeholders. Or that there is a danger that components that are actually technically independent of each other become technically dependent on each other, thus softening the modularization. For software smaller than a bounded context, as in the original example, we need also still another term.
More on that in a moment, before we look at another important factor: teams.
When it comes to teams, there's no getting around Conway's Law. It states: organizations design systems that map the organization's communication structures.4 In short, "You're gonna ship your org chart."
Suppose an organization separates its staff by technical specialty: frontend team, backend team, database team, ... For example, this encourages frontend staff to communicate among themselves so that they share informally and work closely together. However, it insulates them from the back-end staff, with whom they therefore coordinate a lot on documentation and specification. Which leads to the software being structured in exactly the same way: Strictly separate but very broad layers. This organization produces monoliths in a very "natural" way.
This means that if microservices are to cover everything from the frontend to data storage, then one of the basic requirements is that all specialist areas work together as a team. These are so-called "cross-functional teams."
Interestingly, Conway's law also applies in reverse: the structure of the software determines how teams and their members must communicate.
Since team boundaries slow down communication, it doesn't make sense to have multiple teams working together on a microservice. At the opposite extreme, one microservice per developer, frequent coordination between independent services with documentation and specification is necessary. This effectively slows down teamwork, the team becomes a collection of one-person silos.
In summary, all three factors are important for successful microservices: the bounded context, the team structure, and in third place, technology.
Smaller than a bounded context
Back to the original, predominantly technical, example. Each service is a small CRUD backend that barely implements any independent technical concepts or rules. The two contexts of sales and shipping are distributed across service boundaries. There are two names for this. The more advantageous one is "Nanoservices".
Nanoservices cover individual technical functions. But the technical context is not gone! This leads to some problems. For example, the distributed transaction, which not only closely couples two services, but also introduces new sources of error.
The contradiction between domain-oriented context but technical separation leads to three things, among others:
- Business contexts inevitably tightly couple nanoservices technically. Domain-specific changes usually entail modification, testing, and deployment of multiple nanoservices.
- Domain-specific contexts are scattered, and nanoservices have weak cohesion as a result. To understand cohesion, you open more code bases, read more logs, etc.
- All the problems of distributed systems (asynchronous clocks, events in different order, unreliable network, partial failures) are brought into the system at very deep level.
Another view is that this architecture combines the bad features of monoliths with those of distributed systems. The less advantageous name is therefore "distributed monolith".
The three factors as a framework for thinking
If one wants to avoid the disadvantages of nanoservices, the question is how to proceed.
Suppose there is ambiguity about the three factors, for example at the beginning of projects. The greater the lack of clarity, the greater the risk of making decisions out of ignorance that later turn out to be suboptimal and difficult to reverse.
Here, it can help to focus first on working out bounded contexts and team structures, making decisions only as far as clarity allows or postponing some for the time being.5 One possible implementation of this is to start with a non-distributed, modular system. This is easier to adapt to the current state of knowledge and to separate later.6
If you are already in a situation where contradictions between the technical context and the technical separation lead to problems, the three factors described can serve as a "thinking framework". With their help, the contradictions can be identified and approaches to their elimination can be developed.