ITNEXT

ITNEXT is a platform for IT developers & software engineers to share knowledge, connect, collaborate, learn and experience next-gen technologies.

Follow publication

Microservices, reasoning about boundaries

In the previous article, we looked at what we need to know to analyse a microservice application’s correctness and reached the conclusion that it’s next to impossible to map out all the possible outcomes.

In this article, we’ll start looking at what’s needed to stabilise our software if we are going to build a distributed platform and how that leads our hands when deciding on separation.

An important rule shared by most definitions is that microservices must be separated by their “domain model” boundaries. The domain model is the business’s view of the world it operates in, think separate accounting, CRM, inventory microservices. This is an informal definition, where state and behaviour are grouped together by their human-centric meaning. This is useful to make sure that the engineers working on these services have the required understanding of these specific fields in the company, but it doesn’t help with fixing the data issues identified in the previous chapter.

There’s another requirement which comes up when defining boundaries, which is to avoid distributed transactions. This is more formal, so we’ll follow this line of reasoning. Microservices mean independent atomic updates and reads. Failing that, if they shared a single transaction between multiple services, they would rightfully be called a monolith. A distributed transaction is one in which at least two atomically operated microservices are read or updated.

We’ll ignore potential read-consistency issues for the time being (including reads in this definition means no meaningful interaction between microservices) and concentrate on avoiding processes which update multiple services. Any process that modifies any information must only do work on a single microservice and execute these modifications as a single operation.

The information that should be available to more than one microservice can only be shared between the two by one service querying the other each time it requires that information. Anything else would violate our principle of no distributed transactions. Bear in mind that when we say “update”, we mean everything we can potentially read back either directly or indirectly. If we send an update to an external service, can query the information we wrote and store the result in our local service, we’re already breaking this contract.

We know that our software will be unpredictable if we don’t do this (from the previous chapter) but will it be reliable if we do?

Processes serialized through a shared resource

Types of locking

When talking about data consistency on a single multi-process computer, we rely on something called a synchronization primitive. These are exposed to the programming language by the operating system and can be used to make sure that processes are executed in some predictable order (defined by the programmer) or that pieces of information on the computer are kept in-sync. In our distributed system, we also need to make sure that certain processes can be executed sequentially to know whether anyone can reasonably expect us to write predictable software.

Since we already decided to rely on a single microservice for updates, we know they can be reliably ordered. This is similar to what we call an exclusive lock on the information in monoliths. Process A is given exclusive access to the information it updates, other processes trying to access this information in the meantime are made to wait until A finishes all its tasks and terminates. All we’re missing now is the shared lock. A shared lock makes sure that the information it represents is not modified while it’s being looked at. This implies that multiple shared locks can be present on a piece of information, its presence only disallows updates.

Shared locks are useful when we’re basing a decision for an update based on what we read from the database earlier. For example, before I allow a patron to borrow a book from my library, I want to check their outstanding balance. We don’t want another process to put them into debt while storing the information about the borrowed book, since we’re not allowed to do so with a negative balance.

We can‘t do this in our microservice environment. The information we’re relying on to decide any update to the system will either have to be allowed to change during the update or they all must be located in the same single microservice.

In short: The outlined setup is adequate only if we can guarantee that

  • no process will ever update multiple microservices and that
  • information used to make decisions about updates are either never critical (and we can prove that by calculating all their potential states and its knock-on effects) or are in the same microservice

Speaking from personal experience, I’ve never seen a microservice-based project that was successful in even the first requirement, let alone the second one, but anecdotes aren’t statistics. I’m sure there are successes out there.

One thing to take away from all this though is that once one makes the decision to use a microservices based distributed architecture, they can’t easily change their minds without radical changes. This is of course also true for other architectures, but microservices suffer from some unique pain-points, which aren’t usually obvious even to veteran software engineers.

In the next article, we’ll look at how we can go beyond these limitations and what kind of tools are available if we decide to implement distributed software.

Published in ITNEXT

ITNEXT is a platform for IT developers & software engineers to share knowledge, connect, collaborate, learn and experience next-gen technologies.

Written by Andras Gerlits

Writing about distributed consistency. Also founded a company called omniledger.io that helps others with distributed consistency.

No responses yet

Write a response