Over the past few years, Microservices architecture has been all the rage in the software development world. Teams have been breaking their applications down into smaller, independent services with the promise of increased scalability, agility, and easier maintenance. However, as time passes, some teams are realizing that Microservices may not be the right fit for every architecture, and they’re turning back to monolith applications. In this article, we’ll explore why this is happening and provide examples of when microservices may not be the best solution.
The Hype Around Microservices
Before we dive into the reasons for the shift back to monolith applications, let’s take a moment to remember the hype surrounding Microservices. The idea was simple: break a large application down into smaller, independent services that can be deployed and maintained separately. Each service would have its own data store and communicate with other services through APIs.
Microservices promised several benefits, including:
- Scalability: You can scale each service independently, allowing teams to focus resources on areas where they were needed most.
- Agility: Teams could develop and deploy services faster and more frequently.
- Maintenance: You can develop and maintain each service separately, reducing the risk of downtime and allowing teams to fix bugs faster.
Overall, the use of microservices can lead to a more efficient and effective software development process. On the other hand, there are some challenges to consider when moving from a Monolith to a Microservices architecture.
The challenges of Microservices
While Microservices have many advantages, they’re not always the best solution for every architecture. Here are a few reasons why:
- Increased Complexity: Microservices architecture can significantly increase the complexity of an application. With multiple services communicating with each other, developers need to keep track of many moving parts. The more services an application has, the more complex it becomes to develop and test. For sure, microservices makes easier to run Unit Test, but it is way more complex to run Integration Tests. Question: Is your Team ready to invest a good amount of Time in taking the Test environment to the next level?
- No Independent Decisions: One of the benefits of Microservices is the ability for teams to make independent decisions about technology and architecture. However, organizational policies and culture may prevent teams from making these decisions on their own. Therefore, before jumping into a Microservices project, ask yourself “Is your Team meeting regularly with the other Teams to have an comprehensive view of your Ecosystem ?”
- Cost: Developing and maintaining microservices can be expensive. Each service requires its own infrastructure, data store, and team to maintain it. For smaller projects, these costs can be prohibitive, making it more cost-effective to use a monolithic architecture. Question: Have you evaluated the long term picture of these costs as you will not have immediate returns ?
- Technical Debt: Microservices are not a silver bullet for every application. Teams often start breaking their applications down into services before they’re ready, leading to technical debt. If the application isn’t designed with a microservices architecture in mind from the beginning, it can be challenging to break it down into smaller services later. Question: Have you evaluated a possible hybrid approach where you can gradually move your services to a Microservices architecture?
- Operational Overhead: Microservices architecture requires more operational overhead. With multiple services to monitor, manage, and deploy, teams need to invest more resources into their DevOps processes. One crucial aspect relates to loose coupling between services. Question: are you able to deploy your Services independently ?
- Failover: In a Microservice architecture service you can manage service failover by using the underlying infrastructure (for example Kubernetes). However, it can be the case that your services have complex failover rules that you cannot fully adopt in a Microservice context. Question: are your microservices able to failover according to the rules of your Infrastructure ?
The bottom line is, if you have answered “NO” to one or more of the above questions it might be the case you are still not ready to move your current architecture to Microservices.
The Challenge of State Transition
One of the primary challenges of microservices architecture is managing the State Transition between services. In a monolithic application, all components share the same memory space you can manage state transitions directly. In a microservices architecture, however, each service has its own memory space and managing state transitions between them can be complex and expensive.
For example, imagine an e-commerce application where a user places an order. In a monolithic application, the order management and payment processing belongs to the same Domain. That makes relatively easy to manage the state transition.
In a microservices architecture, however, the order management and payment processing would be in separate services, each with its own memory space and state management logic.
This can lead to hidden costs in terms of additional development and operational overhead: for example, developers may need to implement additional messaging or event-driven architectures to manage state transitions between services, adding additional complexity to the application. Additionally, operations teams may need to implement additional monitoring and logging tools to track state transitions and ensure that they’re working correctly.
Another hidden cost of microservices architecture is the increased complexity of testing and debugging. Because each service has its own memory space, it can be more difficult to replicate complex scenarios in UAT environment and identify issues that span multiple services. This can lead to longer testing cycles and more complex debugging processes, which can increase the time and cost of development.
State Transition and Data Consistency
One example where State Transition becomes more complex is about applications that have strong consistency requirements. For example, imagine a banking application that relies on a database to maintain account balances. If multiple instances of the account management service are running, it can be difficult to ensure that all instances have access to the same, up-to-date account balance data.
This is commonly the case of applications that inherently use XA (eXtended Architecture) transactions. You would typically use XA transactions to ensure data consistency across multiple transactional resources, such as databases or message queues. However, implementing XA transactions in a microservices architecture can be complex and expensive. In some can replace them with Event driven architectures that can capture data changes. One good example is Debezium: How to capture Data changes with Debezium
On the other hand, using Event-Driven solutions might not be always possible in some scenarios where:
- There is no Driver to capture changes of your resources: Some systems such as legacy (mainframe) or third-party systems may have limited or no support for capturing data changes or publishing events, making it difficult to integrate them into an event-driven architecture.
- The orchestration cost and network transmission of this information might not meet your SLA. This can be often the case for some applications such as financial, transportations or health care applications which all have strict SLAs in terms of availability, accuracy, and timeliness.
A distributed monolith is an architecture that exhibits the same problems as a traditional monolithic architecture. However it adds extra complexity as it is distributed across multiple machines or clusters. In this architecture, services are tightly coupled and depend on each other, making it difficult to scale, maintain and evolve the system.
For example, consider the SideCar Pattern, which is a well-known pattern for Containers. In this pattern, you are typically deplying a Continer (Sidecar) along the main application Container. The sidecar container provides additional functionality, such as sharing secrets or configuration, without modifying the main container itself.
The SideCar pattern for containers can be useful in certain scenarios, but it also suffers from some of the same limitations as monolith applications. For example, the SideCar container shares the same lifecycle as the main container, which can make it difficult to scale the service independently. If you need to scale up or down the main container you will add additional complexity to the deployment.
Additionally, designing and testing the SideCar container can be more difficult than designing and testing a standalone microservice. Changes to the main container may also require changes to the SideCar container, which can increase development and testing overhead.
Overall, the SideCar pattern is an example of how a Microservice pattern can easily turn into an anti-pattern, aka a Distributed Monolith. Here are the key things to consider:
Allows the coexistence of multiple (possibly polyglot) services in your application. They work quite well for functions like asynchronous logging and asynchronous messaging capabilities.
You are in an anti-pattern if you are using it for synchronous activities that must complete prior to generating a user response. This defeats the purpose of having decoupled Microservices.
When you are using your Sidecars as proxies for other services. There, a change in one of the services, will require a coordination with the whole Microservices architecture making extremely difficult to handle changes.
Monolith vs Microservices might be the wrong comparison
it’s important to recognize that the Microservices vs Monolith is sometimes the wrong way to go. Rather than thinking about monoliths versus microservices, it’s more beneficial to focus on the concept of Bounded Contexts and Deployment Units.
Bounded Contexts refer to the idea that certain parts of an application belong together from a domain perspective. For example, a user authentication service and a payment processing service would likely belong in different conceptual boundaries because they serve different functions. On the other hand, services that handle user profile information and account settings might go together because they both relate to user data.
Deployment units, on the other hand, refer to the idea of breaking an application down into smaller, deployable parts. For example, in a microservices architecture, each service would be a separate deployment unit that you could deploy and scaled independently. In a monolith architecture, you would need to deploy the entire application as a single unit.
Another reason why comparing microservices vs monolith might not be fully appropriate is that they can coexist in the same application. A monolithic application can be broken down into smaller, independently deployable components that communicate with each other using APIs, similar to microservices. This can be achieved by using a modular monolithic architecture, which allows for the separation of concerns and can help to improve scalability and maintainability.
Moreover, microservices and monoliths can also be used together in a hybrid architecture. This approach involves using microservices for certain business functions, while others are implemented as part of a monolithic architecture. For example, an e-commerce platform might use microservices for the checkout process, while the product catalog and search functionalities are part of a monolithic application. This approach allows for flexibility and can help to optimize the architecture for different use cases.
While Microservices architecture has many benefits, it’s not always the best solution for every architecture. Teams need to consider the complexity, cost, technical debt, and operational overhead when deciding whether to use microservices or a monolithic architecture.
Overall, the key is to find the right balance between conceptual boundaries and deployment units. Focusing too much on technology and not enough on domain modeling can lead to poorly defined service boundaries and create issues with service independence. Domain-driven design can help ensure that services are designed around business capabilities, leading to better scalability and maintainability.