How to decompose a monolith
When it comes to microservices, the million dollar question is: “How do I decompose my monolith into microservices?”. Well, as you can imagine this can be done in many ways, and here I’ll be suggesting some guidelines.
The first step of course is the design. We need to establish our service granularity, then decompose our domain into exclusive contexts, each of them encapsulating the business rules and the data logic associated with that part of the business domain. The architect will be responsible of defining the service boundaries - and this is not an easy task. I’d say that decomposing a business domain is an art, rather than a science. On the other hand, in a monolith, it’s not always clear where a service ends and another one starts, as the interfaces between modules are not well defined (there is no need for this).
To identify the microservices we need to build, and understand the scope of their responsibility, listen to the nouns used in the business cases. For example, in e-Commerce applications we may have nouns like Cart, Customer, Review, etc. These are an indication of a core business domain, hence they make good candidates to become microservices. The verbs used in the business cases (e.g. Search, Checkout) highlight actions, so they are indications of the potential operations exposed from a microservice.
Consider also the data cohesion when decomposing a business problem. If you find data types that are not related to one another, they probably belong to different services.
In a real life scenario, if the monolith uses a centralised shared storage (e.g. RDBMS) to store its data, the new architecture does not necessarily imply that every microservice has his own database: it may mean that a microservice is the only once having access to a specific set of tables, related a well specific business case.
As a general principle, when decomposing a monolith, I personally think it’s best to start with a coarse grained granularity, and then refactor to smaller services, to avoid premature complexity. This is an iterative process, so you won’t get this right at the first shot. When services start having too many responsibilities, accessing too many different types of data or having too many test cases, it’s probably time to split one service into multiple services.
My last guideline is not to be too strict with the design. Sometimes aggregation is needed at some point (maybe some services keep calling each other and the boundaries between them is not too clear), and some level of data sharing may be also necessary. Remember, this is not a science, and compromises are part of it.
Challenges and pitfalls
If you made this far, you may have already spotted some potential challenges.
Whether we’re migrating from a monolith, or building a new architecture from scratch, the design phase requires much more attention than in the past. The granularity needs to be appropriate, boundaries definition needs to be bulletproof, and the data modelling very accurate, as this is the base we may decide to build our services on.
Since we’re now in the kingdom of distributed systems, we rely heavily on the network for our system to work correctly; the actual bricks which make our application up are scattered at different locations, but still need to communicate with each other in order to work as one.
In this context, there are many dangerous assumptions we could make, which usually lead to failures. We cannot assume our network is reliable all the time, we have no latency, infinite bandwidth, the network is secure, the topology won’t change, the transport cost is zero, the network is homogenous, and so on. Any of these conditions can happen at any time, and the applications need to be ready to cope with it.
So, the first point is making sure our services are fault tolerant; that means adopting the most common distributed systems implementation patterns, like circuit breakers, fallbacks, client side load balancing, centralised configuration and service discovery.
To have full visibility of the status of our services, good monitoring needs to be in place – and I mean more than before (everything fails all the time, remember?). Compared with monolithic architectures, we may have less complexity on the implementation side (smaller and more lightweight services), but we have more complexity on the operations layer. If the company does not have operational maturity, where we can automate deployments, scale and monitor our services easily, this kind of architecture is probably not sustainable.
Another important factor to consider is that in large distributed systems, the concept of ACID transactions does not apply anymore. If you need transactions, you need to take care of this yourself.
It’s not realistic to think we can guarantee the strong consistency we used to guarantee in monolithic applications (where all components probably share the same relational database). A transaction now potentially spans different applications, which may or may not be available in a particular moment, and latency in data updates is a likely thing to happen (especially when we are adopting an event driven architecture – more on this later).
This means we are aiming to guarantee eventual consistency rather than strong consistency. In a real world business case, more than one service can be involved in a transaction, and every service can interact with different technologies, so the main transaction is actually split in multiple independent transactions. If something goes wrong, we can deal with it by compensating operations.
Some of the most common microservices implementation patterns work particularly well in this context, such as event-driven architectures, event sourcing and CQRS (Command Query Responsibility Segregation)…but these are not the topic of this post. In fact, in next week’s blog post I’ll be looking at these architecture patterns in detail. Make sure you subscribe to catch the final post of this series on microservices.