Recently the first version of a system was put into production, which we helped to develop. This system is based on a microservice-architecture approach.

This approach has recently gained a lot of popularity. And as usual with ‘new things’, they are being hailed as being the best thing since sliced bread. However to realize the true ‘pluses and minuses’, you need to put building a system like this into practice. We did this, and we would like to tell about our experience.

Microservices offer a new approach to the development of large systems. In contrast to the current “monolithic” architecture (one system: one single code base, one big database, deployed as one unit), the microservice approach is suggesting to divide the system into a set of interacting subsystems. In this case, each subsystem (microservice) is developed separately from the others, does not have a common code base with the other, works with its own data, within its own database and is separately deployed in a dedicated container.

Immediately the question pops up, to how to separate the set of system functions into separate pieces. Where these pieces are suitable for making into subsystems and to what size should the fragmentation be made?

The question is important, because if you answer it incorrectly at the beginning of the development project, then instead of solving the problem, you can create more complications. And then the monolith, which normally turns into an uncontrolled “big ball of mud” (BBOM), will simply turn into a “distributed ball of dirt”.

In fact, there’s a whole set of opinions regarding the subject:

  • the size of a microservice is determined by the size of a development team;
  • a microservice should only perform one function (do one thing, single responsibility principle);
  • the size of a microservice should be such, that it’d be easy to replace it, if necessary;
  • a microservice should represent a minimum viable product;
  • a microservice should “easily fit into your head”.

Obviously, these definitions give a certain ‘food for thought’, but, at the same time, do not really help very much in practice. If you try to decide on what and which subsystems using these definitions, it quickly shows that there is a lot of room for controversy.

At the same time, there is a fairly formal way to approach the definition of the size of the microservice. In 2004, Eric Evans wrote the book “Domain-Driven Design”, in which the author was showcasing a set of well-proven approaches to the structuring of complex systems.

For current topic, the “Aggregate” pattern is of particular interest. This pattern is proposed as a way to ensure the observance of invariants (or business rules) related to interconnected groups of objects. In this way, this pattern was successfully used in the software architecture, as part of monolithic systems. When the paradigm shifted, it turned out that the proposed methods of decomposing the original problem into subsystems can be successfully applied in the framework of microsystems

For example, consider a model of ordering of goods. Applying object decomposition, we could end up with the following classes:

  • Customer
  • Product
  • Order
  • LineItem
  • Payment
  • Address
  • Invoice

If we want to implement this system on the basis of microservices, using the definitions which we stated earlier, it is not immediately clear which microservices should be allocated. What single function should each one perform? What is the minimum viable product?

Now let’s try to identify the ‘aggregates’. The following provisions will help us in this:

  • An aggregate is a bounded region of an object graph with a minimum of connections to the remaining graph;
  • the root (the main object) is allocated in the aggregate, through which all operations are performed with the objects included in the aggregate;
  • external objects store a reference only to the root of the aggregate, but not to its internal objects
  • within the boundaries of the aggregate, in any operation, compliance with business rules (invariants) is ensured due to transactional integrity;
  • the aggregate is entirely loaded from the database.

Using this as our starting point, we can get the following scheme of aggregates:

Here, the Customer and the Order are the roots of the ‘Aggregate containers’ through which operations will be performed with the objects included in the unit (LineItem for Order or Payment and Address for the Customer). Direct access is only possible to the root of the unit.

What does this pattern give us as, when applied to the micro-service architecture?
The following:

  • The divisions / containers obtained as a result of such a split are naturally separated from other parts of the system;
  • Due to the transactional consistency within aggregate boundary, we don’t have to get into complicated subject of distributed transactions when performing business operation on microservices. Normally this level of consistency is enough.
  • The resulting size of the microservice itself proves to be consistent with the previous stated definitions on this issue.

This approach has proven itself in practice. At the same time, we would like to draw your attention to the fact that depending on the business requirements it may turn out that it is more practical to increase its size the size of the ‘aggregate’ by combining two or more containers. This might be needed due to the requirements for ensuring the atomic integrity of the data within the same operation performed over several aggregates.

Aggregate-sized microservices are the smallest. Below that level it is impractical to  divide further. The biggest size, to which we could theoretically bring the size of the microservice, without going back to a monolith system, would be a “bounded context” – another notion from the “Domain-Driven Design” book.

In most cases, the use of “eventual consistency” is fully justified for maintaining integrity in operations involving several aggregates (microservices). This is an interesting topic, that deserves a separate consideration.