With the microservices architecture gaining a lot of traction in the software world, more and more companies are migrating from their existing monolith to microservices architecture. Typically, this is a wise move, but great care needs to be taken in order to complete this process successfully. Below, we have gathered the nine most common issues we see when working with our customers on monolith to microservices migration.
1. Having nothing to migrate from
While in some specific systems it might make sense to start development with a microservice architecture, typically it is better to start off with a monolith with well-defined module boundaries and migrate to microservices when the product is more mature. During early development, the approach, requirements, and designs shift a lot. Plus, based on market response, product managers might decide to pursue a different market or niche. Additionally, decision-makers frequently do not have a complete understanding of the requirements from the beginning of the project.
A microservices architecture, due to its distributed and loosely coupled nature, makes introducing system-wide changes much harder than with a monolith. Such groundbreaking changes need to be synchronized across the team and across each microservice. More development effort is required to implement the changes in the code and modify APIs used between services. It can take a lot of time for the system to stabilize after such a migration.
To better understand if your company is ready to migrate to microservices, take a look at this article. It expands on this section with a deeper explanation of who should consider migrating to microservices.
2. Not defining clear objectives and timeline for migration
In larger organizations (those with more than 10 or 20 employees), things tend to fall through the cracks if not managed and tracked against goals. This is particularly true in the case of refactoring initiatives: there is always more immediately pressing work to do than dealing with technical debt and making non-functional changes. Therefore, it is very important to define a strategy and determine precise goals for the microservices migration effort.
The product managers and other stakeholders, when provided a clear plan and goals to achieve, will be able to navigate large numbers of customer requests and issues that come up along the way to accomplish the goals laid out ahead of time. Even if the migration does not go completely to plan (which is quite likely), setting out objectives beforehand is still helpful in getting an idea of where the company is, where it needs to go, and what resources will be required. If the project was not completed in the original timeframe, project managers will ask questions and execute corrective actions to get back on track.
On the other hand, if the migration effort has no concrete scope or timeline, it will be much easier to push back important tasks into the undefined future when the team will have more time and fewer paying customer requests. Unfortunately, this is a shortsighted approach: if the system were fulfilling customer expectations, the microservices migration would not be needed in the first place. In this case, the migration is needed to better serve customers, and a faster migration to microservices is better for the overall business.
3. Not enough (or too much) planning
In addition to the goals and timelines mentioned in the previous section, in order to be able to reach goals in a timely manner, a plan needs to be developed, including the technology side (e.g. technology stack, platform services, tools, etc.), organizational considerations (team organization, project management methodology), and functional analysis (what needs to be migrated, what has the most priority, what changes make sense to introduce in the migration process).
Keep in mind the plan cannot be too detailed–the planning process itself should not require inordinate time and many possibilities cannot be planned for adequately in the fast-paced market. Companies need to keep migration plans on a level that allows them to estimate key milestones and deliverables (e.g. specific microservices deployment) but plans should also be flexible so that changes can be made as the process move along.
If you want to learn more about how to develop an optimal plan to scale your product with microservices, you will find this article interesting.
4. No unified approach to microservices platform
Even though the microservices architecture is all about decoupling and decentralization, some amount of standardization, especially if developed early in the migration process, can be very beneficial. The obvious advantage is that the work related to designing and implementing common services like logging, metrics, health checks, and testing can be done once and then reused by multiple teams. Also, in the short-term, it is easier to move developers across teams or train new ones if requirements change when standards are kept between microservices.
In the long term, some teams can decide to move away from the common tooling. For close to 90% of the teams, the standard setup should be enough to allow them to work effectively. In case of any divergence from the standard, the cost of developing and maintaining the fork should also be taken into account, which might offset the potential efficiency gains.
5. Very tight coupling of services
One common error in the microservices architecture design is applying design patterns of the monolith to microservices platform. While those patterns could work, they will prevent the company from taking full advantage of the resilience of microservices.
For example, I have seen examples of companies implementing a form of distributed three-phase commit transactions across their microservices ecosystem. In this scenario, changes made to the underlying data models are not made permanent until all microservices taking part in the transaction confirm successful execution. While this approach ensures data consistency and immediate failure feedback, it requires that all services are operational, in a healthy state, and not experiencing overload. Conversely, if any of these issues affects one of the microservices (even if it is not critical for the transaction), the whole system might be rendered nonoperational and immediate intervention will be needed to bring it back to order.
If the transaction were implemented as a series of separate operations on microservices, the transaction might be partially completed even if one of the components could not complete its task. In addition, if the issue is permanent, and the transaction cannot be fulfilled at all, a series of compensating actions should be issued to bring the system to the original state.
6. Relying too much on synchronous communication between microservices
One of the key factors that increases the resiliency of any microservices-based system is favoring asynchronous communication over synchronous communication. Synchronous communication is typically a remote procedure call from one service to the other one, like fetching some data from a RESTful endpoint or causing something to occur with a gRPC endpoint. It requires both services to be up and running at the time of the call and it generally also keeps the calling microservice waiting for the result or confirmation of the action being performed. This increases requirements on both the availability and the computing resources of the platform hosting the microservices.
An asynchronous communication channel is most often a message sent by means of a message broker platform to a specific channel. The message broker assures that the messages are durable and are delivered to the microservices subscribed to listen to them. The consumer microservice, upon receiving the message, can act on it and also send a follow-up message or a response back using the same means. This setup differs from synchronous communication in that it puts less strict requirements on the underlying platform: the microservices do not need to be up and running at the same time (the consumer can catch up on the messages later on when it is back online).
Also, the hardware requirements are less demanding: the sender microservice thread is not blocked; when the confirmation or response comes as a message, it will be picked up in the same way as all other communication. Also, if there is a load spike on the service, the incoming messages will just queue up on the channel, instead of resulting in connection timeouts or failures. Moreover, in case of a spike, it is much easier and faster to scale up the consumers by just launching additional instances to process the excess messages in the queue instead of setting up load balancers and reconfiguring the backend instances.
However, there are types of communication in the microservices system that do not play well with asynchronous calls. Those include calls that require immediate feedback to the user (e.g., search as you type or real-time user action feedback) and calls whose main purpose is to return data. Converting those calls to asynchronous does not bring any benefits, i.e., the queuing of the message of a call requiring real-time feedback does not help. Moreover, it introduces additional overhead related to message processing and increased complexity that would affect maintenance effort.
7. Increasing complexity too much by writing too many microservices
While one of the basic principles for microservice system design is “single responsibility”, as in “design your microservices so that one microservice is responsible for a single thing”, it is often applied in an excessively narrow way.
Systems of a mid-level complexity will quite often grow to hundreds of microservices within the first year of development. Each concept, data model, and small groups of functions are quite often created as a separate service, which results in uncontrolled growth of the ecosystem. It can be challenging for developers, QA, DevOps staff, and infrastructure teams to keep track of and maintain all of those services in the long term.
Depending on the project, a better approach to the problem of decomposing a system into microservices could be to think in terms of domains and bounded contexts as defined by domain-driven design. Then, the number of services can be minimized by choosing to implement each domain in just one or a few microservices. While the resulting services might not be quite so small, they are still responsible for a single area of the business domain, so most changes required by the business can be contained within a single service. This approach maximizes the benefits of microservices while minimizing the drawbacks.
8. Not including monolith changes in the migration plan
An important benefit of using a microservices architecture is that you can gradually migrate your system from a monolith to microservices-based application instead of building a new system from scratch while wasting a lot of resources simultaneously maintaining a legacy system. In that case, it is crucial to carefully plan all aspects of coexistence between the microservices and monolith for an extended period of time. Generally, the monolith will require substantial changes upfront to play well with microservices, but this effort will pay off later on when you are able to manage the monolith in a similar way as a microservice. The important aspects of monolith changes include designing or extending an existing API for interservice communication, assuring multiple monolith instances can be run together, and creating a containerization configuration for your monolith so that it can be run next to the other services.
9. Rushing the migration without appropriate expertise
A microservices architecture adds another level of complexity onto other complexities that are present in any software project. For a microservices project to succeed and actually benefit from the microservice architecture, it has to be designed and built according to general best practices adjusted to the particular use case. Resilience, eventual consistency, saga pattern, asynchronous messaging – all of these concepts need to be carefully planned, introduced to the system, and maintained properly for the system to work.
Therefore, it is extremely important that the developers engaged in the migration have previous experience in microservices design or at least have plenty of time to investigate and research the topic. Having a team of microservices veterans that have a proven track record of navigating migration projects through uncertainties is the best approach to make sure that your project is a success. Here you can read some success stories of how others utilized external microservice teams. If you do not have engineers at your company with the requisite experience, we would be happy to help.
Although we covered as much ground as possible in this short article, there is plenty more to be said on the topic of microservices migration. If you found this article useful or interesting, take a look at our ebook with more details on the migration process as well as real-world case studies on implementing microservices in companies of all sizes.