Understanding microservices architecture
Before we discuss microservices architecture, let’s first discuss monolithic architecture. It’s highly likely that you will have encountered or probably even participated in building one. To understand it better, let’s take a scenario and see how it has been traditionally solved using monolithic architecture.
Let’s imagine a book publisher who wants to start an online bookstore. The online bookstore needs to provide the following functionalities to its readers:
- Readers should be able to browse all the books available for purchase.
- Readers should be able to select the books they want to order and save them to a shopping cart. They should also be able to manage their shopping cart.
- Readers should be able to then authorize payment for the book order using a credit card.
- Readers should have the books delivered to their shipping address once payment is complete.
- Readers should be able to sign up, store details including their shipping address, and bookmark favorite books.
- Readers should be able to sign in, check what books they have purchased, download any purchased electronic copies, and update shipping details and any other account information.
There will be many more requirements for an online bookstore, but for the purpose of understanding monolithic architecture, let’s try to keep it simple by limiting the scope to these requirements.
It is worth mentioning Conway’s law, where he stated that, often, the design of monolithic systems reflects the communication structure of an organization:
There are various ways to design this system; we can follow traditional design patterns such as model-view-controller (MVC), but to do a fair comparison with microservices architecture, let’s make use of hexagonal architecture. We will also be using hexagonal architecture in microservices architecture.
With a logical view of hexagonal architecture, business logic sits in the center. Then, there are adaptors to handle requests coming from outside as well as to send requests outside, which are called inbound and outbound adaptors respectively. The business logic has one or more ports, which are basically a defined set of operations that define how adaptors can interact with business logic as well as how business logic can invoke external systems. The ports through which external systems interact with business logic are called inbound ports, whereas the ports through which business logic interacts with external systems are called outbound ports.
We can summarize the execution flow in a hexagonal architecture in the following two points:
- User interface and REST API adaptors for web and mobile invoke business logic via inbound adaptors
- Business logic invokes external-facing adaptors such as databases and external systems via outbound adaptors
One last but very important point to make about hexagonal architecture is that business logic is made up of modules that are a collection of domain objects. To know more about domain-driven design definitions and patterns, you can read the reference guide written by Eric Evans at https://domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf.
Returning to our online bookstore application, the following will be the core modules:
- Order management: Managing customer orders, shopping carts, and updates on order progress
- Customer management: Managing customer accounts, including sign-up, sign-in, and subscriptions
- Payment management: Managing payments
- Product catalog: Managing all the products available
- Shipping: Managing the delivery of orders
- Inventory: Managing up-to-date information on inventory levels
With these in mind, let’s draw the hexagonal architecture for this system.
Figure 1.2 – The online book store application monolith
Though the architecture follows hexagonal architecture and some principles of domain-driven design, it is still packaged as one deployable or executable unit, depending on the underlying programming language you are using to write it. For example, if you are using Java, the deployable artifact will be a WAR file, which will then be deployed on an application server.
The monolithic application looks awesome when it’s greenfield but nightmarish when it becomes brownfield, in which case it would need to be updated or extended to incorporate new features and changes.
Monolithic architectures are difficult to understand, evolve, and enhance because the code base is big and, with time, gets humongous in size and complexity. This means it takes a long time to make code changes and to ship the code to production. Code changes are expensive and require thorough regression testing. The application is difficult and expensive to scale, and there is no option to allocate dedicated computing resources to individual components of the application. All resources are allocated holistically to the application and are consumed by all parts of it, irrespective of their importance in its execution.
The other issue is lock-in to one technology for the whole code base. What this basically means is that you need to constrain yourself to one or a few technologies to support the whole code base. Code lock-in is detrimental to efficient outcomes, including performance, reliability, as well as the amount of effort required to achieve an outcome. You should be using technologies that are the best fit to solve a problem. For example, you can use TypeScript for the UI, Node.js for the API, Golang for modules needing concurrency or maybe for writing the core modules, and so on. Using a monolithic architecture, you are stuck with technologies you used in the past, which might not be the right fit to solve the current problem.
So, how does microservices architecture solve this problem? Microservices is an overloaded term, and there are many definitions of it; in other words, there is no single definition of microservices. A few well-known personalities have contributed their own definitions of microservices architecture:
The definition was published on https://martinfowler.com/articles/microservices.html and is dated March 25, 2014, so you can ignore “sprung up over the last few years” in the description, as microservices architecture has becoming mainstream and pervasive.
Another definition of microservices is from Adam Cockcroft: “Loosely coupled service-oriented architecture with bounded contexts.”
In microservices architecture, the term micro is a topic of intense debate, and often the question asked is, “How micro should microservices be?” or “How should I decompose my application?”. There is no easy answer to this; you can follow various decomposing strategies by following domain-driven design and decomposing applications into services based on business capability, functionality, the responsibility or concern of each service or module, scalability, bounded context, and blast radius. There are numerous articles and books written on the topic of microservices and decomposition strategies, so I am sure you can find enough to read about strategies for sizing your application in microservices.
Let’s get back to the online bookstore application and redesign it using a microservices architecture. The following diagram represents the online bookstore applications built using microservices architecture principles. The individual services are still following hexagonal architecture, and for brevity, we have not represented the inbound and outbound ports and adaptors. You can assume that ports, adaptors, and containers are within the hexagon itself.
Figure 1.3 – The online bookstore microservices architecture
Microservices architecture provides several benefits over monolithic architecture. Having independent modules segregated based on functionality and decoupled from each other unlocks the monolithic shackles that drag the software development process. Microservices can be built faster at a comparatively lower cost than a monolith and are well adept for continuous deployment processes and, thus, have faster time to production. With microservices architecture, developers can release code to production as frequently as they want. The smaller code base of microservices is easy to understand, and developers only need to understand microservices and not the whole application. Also, multiple developers can work on microservices within the application without any risk of code being overwritten or impacting each other’s work. Your application, now made up of microservices, can leverage polyglot programming techniques to deliver performance efficiency, with less effort for more outcomes, and best-of-breed technologies to solve a problem.
Microservices as self-contained independent deployable units provide you with fault isolation and a reduced blast radius – for example, assume that one of the microservices starts experiencing exceptions, performance degradation, memory leakage, and so on. In this case, because the service is deployed as a self-contained unit with its own resource allocation, this problem will not affect other microservices. Other microservices will not get impacted by overconsumption of memory, CPU, storage, network, and I/O.
Microservices are also easier to deploy because you can use varying deployment options, depending on microservices requirements and what is available to you – for example, you can have a set of microservices deployed on a serverless platform and, at the same time, another set on a container platform along with another set on virtual machines. Unlike monolithic applications, you are not bounded by one deployment option.
While microservices provide numerous benefits, they also come with added complexity. This added complexity is because now you have too much to deploy and manage. Not following correct decomposition strategies can also create micro-monoliths that are nightmarish to manage and operate. Another important aspect is communication between microservices. As there will be lots of microservices that need to talk to each other, it is very important that communication between microservices is swift, performant, reliable, resilient, and secure. In the Getting to know Service Mesh section, we will dig deeper into what we mean by these terms.
For now, with a good understanding of microservices architecture, it’s time to look at Kubernetes, which is also the de facto platform for deploying microservices.