"The old order changeth yielding place to new"
- Alfred Tennyson
A well-designed monolithic architecture has been the key to many successful software applications. However, microservices-based applications are gaining popularity in the age of the internet due to their inherent property of being autonomous and flexible, their ability to scale independently, and their shorter release cycles. In this chapter, you will:
- Learn about the basics of monolithic and microservices architectures
- Understand the monolithic-first approach and when to start using microservices
- Learn how to migrate an existing monolithic application to microservices
- Compare and contrast the release cycle and deployment methodology of monolithic and microservices-based applications
Ever since Ada Lovelace (https://en.wikipedia.org/wiki/Ada_Lovelace) wrote the first algorithm for Analytical Engine (https://en.wikipedia.org/wiki/Analytical_Engine) in the 19th century and Alan Turing (https://en.wikipedia.org/wiki/Alan_Turing) formalized the concepts of algorithm and computation via the Turing machine (https://en.wikipedia.org/wiki/Turing_machine), software has gone through multiple phases in its evolution, both in terms of how it is designed and how it is made available to its end users. The earlier software was designed to run on a single machine in a single environment, and was delivered to its end users as an isolated standalone entity. In the early 1990s, as the focus shifted to application software, the industry started exploring various software architecture methodologies to meet the demands of changing requirements and underlying environments. One of the software architectures that was widely adopted was multitier architecture, which clearly separated the functions of data management, business logic, and presentation. When these layers were packaged together in a single application, using a single technology stack, running as a single program, it was called a monolithic architecture, still in use today.
With the advent of the internet, software started getting offered as a service over the web. With this change in deployment and usage, it started becoming hard to upgrade and add features to software that adopted a monolithic architecture. Technology started changing rapidly and so did programming languages, databases, and underlying hardware. Companies that were able to disintegrate their monolithic applications into loosely-coupled services that could talk to each other were able to offer better services, better integration points, and better performance to their users. They were not only able to upgrade to the latest technology and hardware, but also able to offer new features and services faster to their users. The idea of disintegrating a monolithic application into loosely-coupled services that can be developed, deployed, and scaled independently and can talk to other services over a lightweight protocol, was called microservices-based architecture (https://en.wikipedia.org/wiki/Microservices).
Companies such as Netflix, Amazon, and so on have all adopted a microservices-based architecture. If you look at Google Trends in the preceding screenshot, you can see that the popularity of microservices is rising day by day, but this doesn't mean that monolithic applications are obsolete. There are applications that are still suited for monolithic architecture. Microservices have their advantages, but at the same time they are hard to deploy, scale, and monitor. In this chapter, we will look at both monolithic and microservices-based architectures. We will discuss when to use what and also talk about when and how to migrate from a monolithic to a microservices-based architecture.
Monolithic architecture is an all-in-one methodology that encapsulates all the required services as a single deployable artifact. It works on a single technology stack and is deployed and scaled as a single unit. Since there is only one technology stack to master, it is easy to deploy, scale, and set up a monitoring infrastructure for monolithic applications. Each team member works on one or more components of the system and follows the design principle of Separation of Concerns (SoC) (https://en.wikipedia.org/wiki/Separation_of_concerns). Such applications are also easier to refactor, debug, and test in a single standalone development environment.
Applications based on monolithic architecture may consist of one or more deployable artifacts that are all deployed at the same time. Such a monolithic architecture is often referred to as a Distributed Monolith.
For example, a very common monolithic application is a word processing application; Microsoft Word is installed via a single deployable artifact and is entirely built on Microsoft .NET Framework (https://www.microsoft.com/net/). There are various components within word processing application, such as templates, import/export, spell-checker, and so on, that work together to help create a document and export it the format of choice.
Monolithic architecture applies not only to standalone applications, but also to client-server based applications that are provided as a service over the web. Such client-server based applications have a clearly defined multitier architecture that provides the relevant services to its end users via a user interface.
The user interface talks to application endpoints that can be programmed using well-defined interfaces.
A typical client-server application may adopt a three-tier architecture to separate the presentation, business logic, and persistence layer from each other, as shown in the preceding diagram. Components of each layer talk strictly to the components of the layer below them. For example, the components of the presentation layer may never talk to the persistence layer directly. If they need access to data, the request will be routed via the business logic layer that will not only move the data between the persistence layer and the presentation layer, but also do the required processing to serve the request. Adopting such a component-based layered architecture also helps in isolating the effect of change to only the components of dependent layers instead of the entire application. For example, changes to the components of the business logic layer may require a change in the dependent components of the presentation layer but components of the persistence layer may remain intact.
Even though a monolithic application is built on SoC, it is still a single application on a single technology stack that provides all required services to its users. Any change to such an application requires to be compatible with all the encapsulated services and underlying technology stack. In addition to that, it is not possible to scale each service independently. Any scaling requirement is met by deploying multiple instances of the entire system as a single unit. A team working on such a monolithic application scales over time and has to adapt to newer technologies as a whole, which is often challenging due to the rapidly changing technology landscape. If they do not change with the technology, the entire software becomes obsolete over time and is discarded due to incompatibility with newer software and hardware, or a shortage of talent.
Microservices are a functional approach well applied to software. It tries to decompose the entire application functionally into a set of services that can be deployed and scaled independently. Each service does only one job and does it well. It has its own database, decides its own schema, and provides access to datasets and services through well-defined application programming interfaces that are better known as APIs, often paired with a user interface. APIs follow a set communication protocols, but services are free to choose their own technology stack and can be deployed on hardware of choice.
In a microservice environment, as shown in the preceding diagram, there are no layers like in monoliths; instead, each service is organized around a bounded context (https://en.wikipedia.org/wiki/Domain-driven_design#Bounded_context) that adds a business capability to the application as a whole. New capabilities in such an application are added as new services that are deployed and scaled independently. Each user request in a microservices-based application may call one or more internal microservice to retrieve data, process it, and generate the required response, as shown in the following diagram. Such software evolves faster and has low technology debt. They do not get married to a particular technology stack and can adopt a new technology faster:
In a microservices-based application, databases are isolated for each business capability and are managed by only one service at a time. Any request that needs access to the data managed by another service strictly uses the APIs provided by the service managing the database. This makes it possible to not only use the best database technology available to manage the business capability, but also to isolate the technology debt to the service managing it. However, it is recommended for the calling service to cache responses over time to avoid tight coupling with the target service and reduce the network overhead of each API call.
For example, a service managing user interests might use a graph database (https://en.wikipedia.org/wiki/Graph_database) to build a network of users, whereas a service managing user transactions might use a relational database (https://en.wikipedia.org/wiki/Relational_database) due to its inherent ACID (https://en.wikipedia.org/wiki/ACID) properties that are suitable for transactions. The dependent service only needs to know the APIs to connect to the service for data and not the technology of the underlying database.
This is contrary to a monolithic layered architecture, where databases are organized by business capability, which may be accessed by one or more persistence modules based on the request. If the underlying database is using a different technology, then each of the modules accessing the databases have to comply with the same technology, thus inheriting the complexity of each database technology that it has access to.
Database isolation should be done at the database level and not at the database technology level. Avoid deploying multiple instances of the same relational database or graph database as much as possible. Instead, try to scale them on demand and use the isolation capability of these systems to maintain separate databases within them for each service.
The concept of microservices is very similar to a well-known architecture called service-oriented architecture (SOA) (https://en.wikipedia.org/wiki/Service-oriented_architecture). In microservices, the focus is on identifying the right bounded context and keeping the microservices as lightweight as possible. Instead of using a complex message-oriented middleware (https://en.wikipedia.org/wiki/Message-oriented_middleware) such as ESB (https://en.wikipedia.org/wiki/Enterprise_service_bus), a simple mode of communication is used that is often just HTTP.
"Architectural Style [of Microservices] is referred to as fine-grained SOA, perhaps service orientation done right"
- Martin Fowler on microservices
The monolithic layered architecture is one of the most common architectures in use across the software industry. Monolithic architectures are well suited for transaction-oriented enterprise applications that have well-defined features, change less often, and have complex business models. For such applications, transactions and consistency are of prime importance. They require a database technology with built-in support for ACID properties to store transactions. On the other hand, microservices are suited better for Software-as-a-Service, internet-scale applications that are feature-first applications with each feature focused on a single business capability. Such applications change rapidly and are scaled partially per business capability on demand. Transactions and consistency in such applications are hard to achieve due to multiple services, as compared to monoliths that are implemented as single applications.
It is recommended to start with a well designed, modular monolithic application irrespective of the domain complexity or transactional nature. Generally, all applications start as a monolithic application that can be deployed faster as a single artifact and later split into microservices when the application's complexity begins to outweigh the productivity of the team.
The productivity of the team may start decreasing when changes to the monolithic application start affecting more than one component, as shown in the preceding diagram. These changes may be a result of a new feature being added to the application, a database technology upgrade, or the refactoring of existing components. Any changes made to the application must keep the entire team in-sync, especially the deployment team, if there are any changes required in the deployment processes. Communicating such changes in a large team often results in a coordination nightmare, multiple change requests, and in-turn, reduces the overall productivity of the team working on the application.
Productivity also depends on the initial choices made with respect to the technology stack and its flexibility of implementation. For example, if a new feature requires a library that is readily available with a different technology stack or a programming language, it becomes challenging to adopt as it does not conform to the existing technology stack of the application components. In such cases, the team ends up implementing the same feature set for the current technology stack from scratch, and that in turn reduces productivity and further adds to the technology debt.
Before starting with microservices, first set up best design principles among team members. Next, try to evaluate the existing monolith with regard to components and their interaction. If refactoring can help reduce the dependency between the components, do that first instead of disintegrating your application into microservices.
Most applications start as a monolith. Amazon (http://highscalability.com/amazon-architecture) started with a monolithic Perl (https://en.wikipedia.org/wiki/Perl) /C++ (https://en.wikipedia.org/wiki/C%2B%2B) application, and Twitter (http://highscalability.com/blog/2013/7/8/the-architecture-twitter-uses-to-deal-with-150m-active-users.html) started with a monolithic Rails (https://en.wikipedia.org/wiki/Ruby_on_Rails) application. Both organizations have not only gone through more than three generations of software architectural changes, but have also transformed their organizational structures over time. Today, all of them are running on microservices with teams organized around services that are developed, deployed, scaled, and monitored by the same team independently. They have mastered continuous integration and continuous delivery pipelines with automated deployment, scaling, and monitoring of services for real-time feedback to the team.
The top-most challenge in migrating from a monolithic application to microservices is to identify the right candidates for microservices. A well structured and modularized monolithic application already has well-defined boundaries (bounded contexts) that can help disintegrate the application into microservices. For example, the User, Orders, and Interest modules already have well-defined boundaries and are good candidates to create microservices for. If the application does not have well-defined boundaries, the first step is to refactor the existing application to create such bounded contexts for microservices. Each bounded context must be tied to a business capability for which a service can be created.
Another approach in identifying the right candidates for microservices is to look at the data access patterns and associated business logic. If the same database is being updated by multiple components of a monolithic application, then it makes sense to create a service for the primary component with associated business logic that manages the database and makes it accessible to other services via APIs. This process can be repeated until databases and the associated business logic are managed by one and only one service that has a small set of responsibilities, modeled around a business capability.
For example, a monolithic application consisting of User, Interest, and Orders components can be migrated into microservices by picking one component at a time and creating a microservice with an isolated database, as shown in the preceding diagram. To start with, first pick the one with the least dependency, the User module, and create the User Service service around it. All other components can now talk to this new User Service for User Management, including authentication, authorization, and getting user profiles. Next, pick the Orders module based on the least dependency logic, and create a service around it. Finally, pick the Interest module as it is dependent on both the User and Orders modules. Since we have the databases isolated, we can also swap out the database for Interest with maybe a graph database that is efficient to store and retrieve user interests due to its inherent capability of storing relationships as a graph.
Once a monolithic application is disintegrated into microservices, the next step is to deploy them into production. Monolithic application are mostly deployed as a single artifact (JARs, WARs, EXEs, and more) that are released after extensive testing by the quality assurance (QA) team. Typically, developers work on various components of the application and release versions for the QA team to pick and validate against the specification, as shown under the Org Structure of monolithic architecture in the following diagram. Each iteration may involve the addition or removal of features and bug fixes. The release goes through multiple developers (dev) and QA team iterations until the QA team flags off the release as stable. Once the QA team flags off the release, the released artifact is handed over to the IT ops team to deploy it in production. If there are any issues in production, the IT ops team asks the dev team to fix them. Once the issues are fixed, the dev team tags a new release for QA that again goes through the same dev-QA iterations before being marked as stable and eventually handed over to IT/ops. Due to this process, any release for a monolithic applications may easily take up to a month, often three months.
On the other hand, for microservices, teams are organized into groups that fully own a service. The team is responsible for not only developing the service, but also for putting together automated test cases that can test the entire service against each change submitted for the service. Since the service is to be tested in isolation for its features, it is faster to run entire test suites for the service for each change submitted by the developers. Additionally, the team itself creates deployable binaries often packaged into containers (https://en.wikipedia.org/wiki/Linux_containers), such as Docker (https://en.wikipedia.org/wiki/Docker_(software)), that are published to a central repository from where they can be automatically deployed into production by some well-known tools, such as Kubernetes (https://en.wikipedia.org/wiki/Kubernetes). The entire development to production timeline is cut short to days, often hours, as the entire deployment process is automated. We will learn more about deploying microservices in production and how to use these deployment tools in Part-4, the last part of this book.
There is a reason why a lot of microservice projects fail and only a few succeed. Migrating from a monolithic architecture to microservices must not only focus on identifying the bounded contexts, but also the organizational structure and deployment methodologies. Teams must be organized around services and not projects. Each team must own the service right from development to production. Since each team owns the responsibility for testing, validation, and deployment, the entire process should be automated and the organization must master it. Development and deployment cycles must be short with immediate feedback via fine-grained monitoring of the deployed microservices.
In this chapter, we learned about monolithic and microservices architectures and why microservices are becoming popular in the industry, especially with web-scale applications. We learned about the importance of database isolation with microservices and how to migrate a monolithic application to microservices by observing the database access pattern. We also discussed the importance of the monolith-first approach and when to move towards microservices. We concluded with a comparison of monolithic and microservices architectures with regard to the release cycle and deployment process.
The next chapter of this book will talk about microservice architecture in detail; we will learn more about domain-driven design and how to identify the right set of microservices. In Chapter 3, Microservices for Helping Hands Application, the last chapter of Part-1, we will pick a real-life use case for microservices and discuss how to design it using the principles of microservice architecture.