In this article, we analyze the move from monolithic to microservices architectures, deep-dive into microservices communication types, and examine the best practices for communication between services with a retail application example.
Disclaimer: This article is based on my personal experience and knowledge and there’s no definite “right” or “wrong”. Remember that you should always check what’s suitable for your application and business needs.
The buzzword “Microservices” has been around the software industry for a long time but first, let’s review what was before – the monolithic architecture.
Monolithic architecture means that your application consists of one single codebase, and you deploy that codebase as one single unit.
As you can see in the image above, all requests that are coming from a user’s browser, mobile device, or an API, will go through the router and maybe some middleware (auth middleware, for example). Then the router decides which specific logic the request should be passed and processed, and this logic will determine what to do: if to store some data to a database or perhaps reading some data from the database. Eventually, we build a response and send it back to whoever made the request.
Monolith Characteristics and Components
A monolith contains:
- Business Logic
- Database access
Those characteristics should be implemented in a way that supports all of the features in our app.
Monolith Challenges and Limitations
This approach has some problems:
- Reliability: An error in any of the modules in the application can bring the entire app down. (if the DB crashes all the features that depend on the DB will not work)
- Updates: Due to a single large codebase and tight coupling, the entire application would have to deploy for each update
- Technology stack: A monolithic application must use the same technology stack throughout. Changes to the technology stack are expensive, both in terms of the time and cost involved
Microservices architecture means that your application consists of one or more codebases, and you deploy them independently.
Microservices Architecture Characteristics and Components
Microservices architectures contain:
- Business Logic
- Database access
Those characteristics should be implemented in a way that supports only one feature in our app.
As you can see in the diagram above, every microservice has its router, business logic, data access, and even its database.
The most significant advantage of this approach is that in case a service inside your application crashes or just mysteriously disappears, other services will continue working properly, and your application loses only that specific feature from your entire application.
Monolithic to Microservices Architectures and Code
The difference between monolithic and microservices approach is that a monolith has all the code needed to implement every feature in the application when a microservice has all the code needed to implement only one feature in the application.
The challenge with microservices is data management between services. Ok… but why?
To understand that, I would like to focus on two things that almost every application does:
- Data access
- Data Storage
How do applications access and store data?
As we learned previously, and as was shown in the microservices architecture diagram, there is a basic rule regarding databases in microservices: each microservice has its own database.
Why is that?
Advantages of DB per microservice
- We want each service to run independently from other services. If all the services depend on the same DB and it crashes, all the services will be down, and we will lose the decoupling advantage of microservices architecture.
- The database’s schema might change unexpectedly. If many teams develop over the same DB and one of the teams decides to change the schema of one of the tables, it will crash the other team’s services that rely on the old schema.
- Some services might function more efficiently with different types of databases. For example, for some services, SQL may be the best fit when for other services NoSQL will fit better.
Now we understand why we want to have our own database per service (and that we store data per service).
But how do we access data between different microservices? To answer this question, we’re going to understand how *not to* access data.
Every service has its own DB, and although we could technically reach another service’s DB from a specific service, it’s mandatory that services will never directly reach each other’s databases. If one service reaches another one’s database that will cause high coupling between services, and the advantages mentioned above will not apply.
How *should* we access data between services?
Let’s look at an example of a retail application where you can place an order to buy video games.
The microservices architecture will split into three services:
- User management service – Responsible for user sign-in and sign-up
- Video games service – Responsible for the list of available video games
- Orders service – Responsible for purchasing video games
Each of the microservices has its own database, and it looks like the diagram below:
All looks easy and straightforward, but let’s say that we want to add another feature to our application. We want to show what video games were ordered by a particular user. For this feature, we need to collect data from users, video games, and the orders database. How can we create that new service when we can’t access other services databases? That’s why data management between services can be very challenging.
For communication between services we have two methodologies we can use:
- Synchronous Communication
- Asynchronous Communication
Synchronous communication is when the sender submits a direct request and then waits for processing and some kind of reply, and only then proceeds to other tasks.
This is typically implemented as REST calls, where the sender submits an HTTP request, and then the service processes this and returns an HTTP response.
* (Note: not only HTTP is a type of direct request between services)
Service D in the example below is responsible for showing video games that were ordered by a particular user:
For every request that comes in (for example, “show me orders of user with id number 1”) it will send an HTTP request to user management to get information about that user, an HTTP request to get all orders of that specific user, and a third request to get the details of the game that were ordered in that specific order.
Now, let’s talk about the pros and cons of that approach.
Synchronous Communication Pros
- Conceptually easy to understand
- The new service will be without a DB, and you’ll be able to save on costs
Synchronous Communication Cons
- If any inter-service request fails, the overall request fails. For example, if the service of the orders is down, our new service will not work.
- The entire requests are only as fast as the slowest request. For example, if the video games service took much more time then other services, you will need to wait until you get the answer from that service.
- Scale dependency. For example, if your new service needs to keep pace with a big amount of requests per second, other services should be kept at that speed too, and it can result in costs
- It can easily introduce webs of requests. For example, if you have a lot of services (much more than 4), you will quickly have a big web diagram and dependency web between all the services, and it might be very hard to keep track of which service depends on whom.
Asynchronous communication means that a service doesn’t need to wait for another to conclude its current task. A sender doesn’t necessarily wait for a response but either poll for results later or record a callback function or an action. This is typically done over message buses like Apache Kafka and RabbitMQ.
* Note: Pub/Sub, Event bus, and Queue are only some of the options that you can use for async communication.
In this example, I will use a Pub/Sub approach.
Pub/Sub and its purpose
Pub/Sub is an asynchronous messaging service that decouples services producing events from services processing events. Pub/Sub offers durable message storage and real-time message delivery with high availability and consistent performance at scale.
First of all, every one of our services (A — UserManagement, B — VideoGames, C — Orders) is gonna publish an event after creating a request:
- User Management service will publish an event with user data (For example, User Name, User Id) after a new user signs up.
- Video Games service will publish an event with game details when a new video game is created (For example, Video Game id, video game name).
- Orders service will publish an event with orders data (For example, order id, video game id, user id) after a new order is created.
Our new service will be a subscriber to those events, and every new event that is written above is fired. Our new service will handle them and save them inside an internal DB. It means that we will duplicate some data and store it.
Yes, it’s a very specific database schema for a very specific feature that was requested.
For a given user id, we would like to show the video game and image of every product that the user has ever ordered.
Now, let’s review the pros and cons of that approach.
Asynchronous Communication Pros
- Service D has zero dependencies on other services. In this approach, if any other service will go down, our service will still work and return a response to the request).
- Service D will be extremely fast. It will depend only on the internal DB/service performance without any external dependency.
Asynchronous Communication Cons
- Data duplication. You will duplicate some data as in our example: the image URL and video game name, and maybe if the video game name is changed, you’ll need to publish a new event and update that data.
- Pay for extra storage and for an extra DB. In my opinion, nowadays the price of storage is not so high and in some cases extremely cheap, so it is not really a disadvantage.
- Harder to understand. This idea of pub/sub is harder to understand.
Observability within Microservices
Microservices split application functionality into independent services and each service can be easily and cost-effectively changed, deployed, scaled, and managed as a service. The downside of microservice architecture is that they also introduce operational complexity when it comes to monitoring service-to-service communication and diagnosing performance issues. The result? Downtime, latency, and longer delivery cycles.
To monitor, troubleshoot, and fix problems quickly, developers need to view the entire architecture and its components. By stitching together metrics, logs, and traces, observability addresses the challenges of monitoring highly distributed environments.
Epsagon automatically maps the discovered application stack into a highly visual architecture view called a Service Map. You can drill into a component or node to discover a problem, see metrics, analyze trends, and better understand issues.
With Epsagon’s Trace Search, you can query and search any call using criteria such as name, time, resource, exception, user ID, or payload. You also can see the analysis for the search as Request Errors and Duration stats. It’s all correlated and with no manual work, setup, configuration, or maintenance.
In this article, we reviewed the migration from monolithic to microservices architectures and deep-dived into challenges and the pros and cons of different communication approaches. In addition, we demonstrated the different ways available to communicate between microservices through an example of a simple retail store application.