A deep dive into microservices and its key elements
Traditionally, in software development, applications have developed as a single unit or monolith. All of the components are tightly coupled, and a change to one component threatens to have rippling effects throughout the code base and functionality. This makes long-term maintenance a major concern and can hinder developers from rolling out updates quickly.
Microservices will have you assess that monolith, break it into smaller and more perceivable applications. Each application will relate to a subsection of the larger project, which is a called domain. We will then develop and maintain the code base per application as independent units. Typically, microservices are developed as APIs and may or may not interact with each other to complete operations being carried by users through a unifying user interface. Typically, the microservice architecture comprises a suite of small independent services, which communicate via HTTP (REST APIs) or gRPC (Google Remote Procedure Call). The general notion is that each microservice is autonomous, has a limited scope, and aids in a collectively loosely coupled application.
Building a monolith
Let’s imagine that we need to build a health facility management web application. We need to manage customer information, book appointments, generate invoices, and deliver test results to customers. If we were to itemize all the steps needed to build such an application, key development and scoping activities would include the following:
- Model the application, and scope the requirements for our customer onboarding, user profiles, and basic documents.
- Scope the requirements surrounding the process of booking an appointment with a particular doctor. Doctors have schedules and specialties, so we have to present the booking slots accordingly.
- Create a process flow for when a match is found between a customer and a doctor. Once a match is found, we need to do the following:
- Book the doctor’s calendar slot
- Generate an invoice
- Potentially collect a payment for the visit
- Send email notifications to the customer, doctor, and other relevant personnel
- Model a database (probably relational) to store all this information.
- Create user interfaces for each screen that both customers and the medical staff will use.
All of this is developed as one application, with one frontend talking to one backend, one database, and one deployment environment. Additionally, we might throw in a few third-party API integrations for payment and email services. This can be load balanced and hosted across multiple servers to mitigate against downtime and increase responsiveness:
Figure 1.1 – Application building
However, this monolithic architecture introduces a few challenges:
- Attempts to extend functionality might have ripple effects through multiple modules and introduce new database and security needs.
Potential solution: Perform thorough unit and integration testing.
- The development team runs the risk of becoming very dependent on a particular stack, making it more difficult to keep the code base modern.
Potential solution: Implement proper versioning strategies and increment them as the technology changes.
- As the code base expands, it becomes more difficult to account for all of the moving parts.
Potential solution: Use clean architectural methods to keep the code base loosely coupled and modular.
The reality is that we can overcome some of these challenges with certain architectural decisions. This all-in-one architecture has been the de facto standard, and frankly, it works. This project architecture is simple, easy enough to scope and develop, and is supported by most, if not all, development stacks and databases. We have been building them for so long that perhaps we have become oblivious to the real challenges that prevail as we try to extend and maintain them in the long term.
Figure 1.2 shows the typical architecture of a monolithic application:
Figure 1.2 – One user interface is served by an API or code library with business logic and is serviced by one database
Now that we have explored the monolithic approach and its potential flaws, let us review a similar application built using microservices.
Building microservices
Now, let us take the same application and conceptualize how it could be architected using microservices. During the design phase, we seek to identify the specific functionalities for each tranche of the application. This is where we identify our domains and subdomains; then, we begin to scope standalone services for each. For example, one domain could be customer management. This service will solely handle the user account and demographic information. Additionally, we could scope bookings and appointments, document management, and finally, payments. This then brings another issue to the foreground: we have dependencies between these three subdomains when we need service independence instead. Using domain-driven design, we then scope out where there are dependencies and identify where we might need to duplicate certain entities. For instance, a customer needs representation in the booking and appointments database as well as payments. This duplication is required if we are using separate databases per service (which is strongly encouraged).
The microservices require us to properly scope the flow of operations that involve multiple services playing a part. For instance, when making a booking, we need to do the following:
- Retrieve the customer making the booking.
- Ensure that the preferred time slot is available.
- If available, generate an invoice.
- Collect the payment.
- Confirm the appointment.
That process alone has some back-and-forth processing between the services. Properly orchestrating these service conversations is very critical to having a seamless system and adequately replacing a monolithic approach. Therefore, we introduce various design patterns and approaches to implementing our code and infrastructure. Even though we break potentially complex operations and workflows into smaller and more perceivable chunks, we end up in the same position where the application needs to carry out a specific operation and carry out the original requirements as a whole.
Figure 1.3 shows the typical architecture of a microservices application:
Figure 1.3 – Each microservice is standalone and unifies in a single user interface for user interactions
Now that you are familiar with the differences between the monolithic and microservices approaches, we can explore the pros and cons of using the microservices design pattern.