From Monolith to Microservices
Microservices are great, but is refactoring your monolithic architecture worth the time? Read on to find out.
Join the DZone community and get the full member experience.Join For Free
Microservices are an architectural style focused on the speed of software development defined as the number of functionalities created within a time unit or as the duration of the whole delivery process – from concept to deployment (time to market). The current high changeability of business environments fosters increasing popularity of the microservice approach, which forces companies to react quickly in order to avoid the situation when a good solution, but implemented too late, becomes a bad solution.
Most of today’s enterprise-class systems have a monolithic architecture. Their indisputable advantage is, of course, the fact that they work and generate income or savings to the companies which own them. However, as the systems grow, a monolithic architecture makes the pace of their development gradually decrease. Business owners must wait longer for the functionalities they have ordered. To make matters worse, scalability of the software development process turns out to be far from linear. Engaging more people or teams to work on such systems generates fewer and fewer benefits. Introducing new employees takes more and more time, while the existing ones become discouraged and demand extra pay for harmful working conditions or start to ponder a career path outside the organization. Such symptoms clearly indicate that the system's architecture has ceased to meet the company’s requirements. Applying an evolutionary architecture, such as microservices, is the best solution to address the issue of an insufficient pace of system development.
To illustrate the effect of using a microservices approach, we can use a certain metaphor. Let’s assume that we would like to build and maintain a space station. In order to do that we will need to deliver various cargoes to orbit, such as people, materials, equipment, etc. At present, the only available form of transport are space flights, which, despite the latest achievements of companies such as SpaceX, are very expensive and require time-consuming preparations. Hence, we can try to come up with another solution – a space elevator. Obviously, expenses for building it will be much higher than the cost of a single flight. However, each subsequent transport will be possible basically right away and free of charge (compared to flight costs).
The metaphor, apart from illustrating a vision of the bright future which microservices promise to us, allows us to come to another important conclusion. Namely, implementing such an approach constitutes a huge challenge and requires significant investment. Therefore, before we start to build a space elevator, we should make sure that we need to get to the orbit and will be going there frequently. Otherwise, the whole endeavor will merely be art for art’s sake.
To Migrate or Not to Migrate?
Before making a decision to apply the microservices approach, you should consider a few issues:
- If the product (system) is market-proven.
- If the expected pace of product development requires engaging more than one team (~10 people).
- If the system has high requirements related to reliability and scalability or whether they vary significantly for its individual elements.
Sam Newman, a widely acknowledged author of the microservice architecture theory, has come up with the following definition:
Small autonomous services modeled around a business domain that work together.
It turns out that the basic building blocks in this approach are the services which we will extract using decomposition by domain (business capability). These services can be developed and deployed independently, but they must cooperate in order to implement a business process.
If the component, which is part of the system, merely stores data, then it is basically a database; if it contains only logic, it is called a function. A service, on the other hand, comprises both of these elements – logic and data. Such a combination creates the foundation for the autonomy which Sam Newman describes. It is worth having this definition in mind while approaching the issue of system decomposition.
Architectural styles differ from each other in the way they decompose a system into smaller components. A significant aspect of microservices architecture is the organization of service functionalities around business capabilities, which allows developers to provide high cohesion and stability for the established division. Such methods of harnessing the complexity of business logic were popularized by Eric Evans under the name “Domain Driven Design.” It describes the way of dividing the domain into subdomains and then designating bounded contexts within them which will be used as service boundaries.
A practical technique of identifying bounded contexts is “Event Storming” suggested by Alberto Brandolini. Its first step involves identifying events occurring in the business domain. Such an approach allows devs to direct the modeling process to be behavior-based instead of focusing on the static structure of the information being processed. This seemingly subtle change of perspective is crucial for microservices architecture because it enables the development of a system characterized by loose coupling and autonomy of its individual services.
When setting the boundaries of services you should not forget that they will become boundaries of transactions and of strong, immediate consistency (ACID). For operations which involve several services, the system will provide BASE (Basically Available, Soft state, Eventual consistency) semantics, which offers the guarantee of liveliness, but does not provide safety. Unlike ACID, BASE means that the system will eventually achieve consistency, however, it is neither known how such a state is going to look nor how the system will behave in the meantime. There is a possibility of achieving strong, eventual consistency within the BASE model, without traditional mechanisms for controlling concurrency. However, it requires using conflict-free replicated data types (CRDT).
Reality is not transactional. Users don't require immediate consistency too often. An example can be found, however, in the finance domain, which, as it may seem, should have the highest consistency requirements. Nevertheless, we all got used to the fact that making interbank transfers takes hours or even days and we do not know what happens with funds during this operation – we can neither see it on the source account nor on the target one.
Many times software engineers themselves tend to force immediate consistency where it is unnecessary, and sometimes it may be even harmful. Once I came across a large company management system. While granting the customer a VIP status within the CRM module, the logistics subsystem automatically generated the order to ship a bottle of champagne, which was supposed to be distinguished as a separate transaction. The whole operation was performed as a single transaction, however, which caused the warehouse to run out of supplies and when an attempt was made to order more we ended up with an error and the whole transaction was rolled back. In this case, dividing operations into two separate transactions and using a saga pattern would be a better solution.
To sum up, you should check what consistency guarantees are required for particular functionalities and make sure that the established boundaries secure these requirements.
Introducing a microservices architecture comes with a lot of challenges, which should be discussed before the refactoring process starts. A system designed this way will require automation of build, configuration, testing, and deployment processes. Also, tools for collecting and aggregating logs as well as metrics and behavior analyses (tracking, profiling etc.) within distributed environments will be crucial. Long story short, you will need DevOps culture in place.
At the early stage of the transformation process, it is necessary to determine how to integrate and coordinate services, data architectures, methods of providing transactional consistency and reliability, configuration, service discovery, as well as other cross-functional aspects. Introducing or changing these solutions at later transformation stages will be much more costly than at the beginning of the process.
The issues mentioned above are so important and broad that they should be discussed in a separate article. Neglecting these areas, especially integration, may result in a lack of service autonomy and result in a distributed monolith instead of a microservices architecture.
Evolution or Revolution?
Once we have set the service boundaries, which are going to act as the target solution structure, we must decide how to carry out the transformation: whether we are going to renew the system step-by-step, by gradually extracting subsequent components, or create the whole system from scratch and put it into service after the whole operation is complete. The second option is certainly much simpler and more tempting, however in most cases unacceptable. In conditions of strong competition and huge dynamics of the market, only a few companies can allow themselves to suspend, for a longer period of time, the development of the IT system which their key business processes depend on. If, along with transformation of the architecture, we want to additionally change the technology or a key framework, then the first approach cannot be applied either. In such a situation, we may adopt an approach called the strangler pattern.
How can we perform a gradual transformation to a microservices architecture?
Let’s take the system with a monolithic structure as a starting point for the process:
The first step is a partial, logical separation of the user interface from the service layer. Handling of the business logic commands is delegated to the service layer, while queries, which support the views, are directed to the database. At the moment, we are not modifying the database itself:
The second step is a full, logical separation of the user interface:
In the third step, you have to physically separate the user interface, create an API on the backend, and use it to communicate between these two components:
The fourth step involves gradual extraction of subsequent services. This time we perform a full separation – down the database level. If the organization had not used a microservices architecture earlier, it would be good to start decomposing with a domain which is small and easy to extract. It will allow the team to gain the necessary experience with little risk and in a relatively short time.
The last step is front-end decomposition, which we may also perform in stages – by gradually extracting subsequent user interface elements:
An indisputable advantage of the this process is its evolutionary character. Thanks to this process we may gradually change the architecture, without fully stopping system development, by adjusting the pace of changes to business requirements and available resources.
System architecture does not exist in a void. It is strongly tied to the development process and the company’s structure and culture. The key feature of a microservices architecture is its evolutionary character, which means that choosing this approach will bring the most benefits to a company comprised of little autonomous teams using Agile software development methodologies. However, a lack of these features should not stop us from selecting a microservices architecture. We may simultaneously adopt Agile principles while changing the architecture, but we cannot completely ignore them. According to Conway's Law:
Organizations which design systems… are constrained to produce designs which are copies of the communication structures of these organizations.
Thus, if we introduce a microservices approach in a strongly centralized, hierarchical company, there is a risk that, with time, our system’s architecture will drift towards a monolith. That is why we should start transforming the system architecture with the so called Inversed Conway Maneuver, i.e. developing an organizational structure which is isomorphic with the expected target system architecture. Once we remove traditional functional silos (front0end devs, backend devs, DBA, QA, Ops, etc.), and we introduce cross-functional teams, focused around value streams and business capabilities, it will be much easier for us to decompose the system analogically, and then maintain obtained architecture.
Once we have completed the transformation process, it is worth checking if we managed to get the expected result. The goal of introducing a microservices architecture is, first of all, to improve processes related to developing and maintaining software. We might measure it by following a few simple indicators, for example:
- Duration of the production cycle defined as the average time from concept to implementation (time to market).
- Performance of the production process measured as the average number of functionalities (user stories) provided by the team (or per team member) in a time unit.
- Scalability of the production process.
- Average time necessary to locate and remove failure (mean time to repair).
Comparing the values of these characteristics for old and new architectures, we might evaluate the effect of the transformation(s) performed. The abovementioned indicators can also be monitored during the transformation process.
As engineers, we are fascinated with solutions which allow us to improve the world around us. However, we should be also aware of the fact that each change is an investment which has to pay off.
Opinions expressed by DZone contributors are their own.