In today’s rapidly evolving digital landscape, agility, scalability, and resilience are paramount for business success. Many organizations are transitioning from monolithic architectures to microservices to meet these demands. This shift, while rewarding, requires careful planning and execution.
This blog post will explore the why, what, and how of transitioning from a monolithic to a microservices architecture. We will also share our experience in modernizing one of our enterprise applications by transitioning it from a monolithic architecture to a microservices-based architecture.
The transition from a monolithic to a microservices architecture is often driven by the need for greater scalability, flexibility, and speed. Some common reasons for making this shift include:
- Scalability: As your application grows, it becomes challenging to scale a monolith efficiently. Microservices allow you to scale only the parts of the application that need more resources.
- Faster Development Cycles: Microservices enable different teams to work on different services independently, speeding up development and deployment times.
- Resilience: Microservices can improve the fault tolerance of an application, as issues in one service are less likely to impact the entire system.
Monolithic Architecture.
A monolithic architecture is a traditional way of building applications as a single, unified unit. In a monolithic application, all components and functionalities—such as user interfaces, data access, and business logic—are tightly coupled and operate as a single entity.
Read more about monolithic: https://cloud.google.com/architecture/microservices-architecture-introduction#monolithic_applications
What Are Microservices?
Microservices architecture is an approach where an application is built as a collection of loosely coupled, independently deployable services. Each service is responsible for a specific piece of business functionality and can be developed, deployed, and scaled independently.
Read more about microservices: https://cloud.google.com/architecture/microservices-architecture-introduction#microservices-based_applications
Steps to Transition from Monolithic to Microservices
Transitioning to microservices is not a one-size-fits-all process. However, the following steps can guide your journey:
1. Assess and Plan
Evaluate your current system to identify pain points in your monolithic architecture and determine if microservices are the right solution. Set clear objectives for what you want to achieve with microservices, such as improved performance, scalability, resilience, or faster release cycles. Develop a roadmap for gradually breaking down your monolith into microservices.
How we did it: In one of our enterprise products which was a monolithic application, the client faced scaling issues as there were certain sets of API calls that were exposed to its channel partners and had heavy usage. This led to significant memory spikes on the EC2 instance, ultimately affecting the entire application’s performance. The API gateway was limited to 100 requests per second with a burst capacity of 20 in order to limit the concurrency. Simply increasing the rate limit and scaling the whole application to accommodate a few heavily used APIs wasn’t an ideal solution. Thus, we created a plan to separate these heavily used external APIs and containerize these as individual microservices to enable independent evolution and autoscaling without impacting other resources.
2. Design Microservices
Determine the right level of granularity for each service—too coarse, and you lose the benefits; too fine, and you increase complexity. Decide how services will communicate (e.g., synchronous REST, asynchronous messaging). Define how data will be stored, accessed, and shared between services. Consider using databases per service. Create sufficient isolation to allow system room to evolve. Isolation could be at process level, network level and data level.
Here are a few important considerations for the design phase.
- Identify natural domain boundaries and protect them with APIs.
- Structure Microservices as independent applications that expose APIs, typically via REST or asynchronous event-driven mechanisms.
- Exclusive data ownership: Each domain must own its data exclusively, with access restricted to the respective microservice..
- Reduce dependencies and load on services providing APIs, enabling greater flexibility, easier client creation, innovative deployment strategies, and autoscaling.
- Isolate microservices by deploying them in separate containers or on different machines to prevent resource competition and break coupling.
- Contain failures within microservices, ensuring that only dependent services are impacted, not the entire system.
How we did it: We established boundaries in our application using domain boundaries. For example, when making a reservation, the resource and its availability for a particular time slot were crucial for verifying availability for both the resource and channel partner. We carved out an external-facing microservice for “reservation” and two helper microservices for “resource” and “channel partners”. For intercommunication with helper microservices, we implemented gRPC, which offers the benefit of low latency.
3. Decomposing the monolith
Decompose by Business capabilities
Break down the monolith by leveraging your organization’s business processes or capabilities that create value and generate revenue. Most organizations have multiple business capabilities, which vary by industry or sector. This approach works best if your team has a deep understanding of the business units.
How we did it: The diagram below illustrates the decomposition of our monolithic application into microservices, organized by business capabilities, such as Locations Management, Rates Management, Promotional, Reservations and Subscriptions(Monthly and Access Codes).
Decompose by sub-domain
DDD(Domain-Driven Design) uses the subdomain pattern to identify bounded contexts and define the scope of each microservice. Decompose the Monolith, start by identifying and extracting the most critical or easiest-to-extract functionality as independent services. This approach works well for existing monolithic systems that have clearly defined boundaries between modules associated with these subdomains.
How we did it: The diagram visually represents how our monolithic application was divided into microservices based on different business sub-domains. The Reservations domain was further subdivided into Channel Partner and Locations Inventory subdomains. A similar approach was applied to Rates management and Promotional modules.
4. Migration Strategies
Begin by migrating a small, non-critical service to test your microservices architecture. Gradually migrate more services, learning and adapting your approach as you go. Implement monitoring and logging to track the performance of your microservices and optimize them as needed. Below are a few approaches discussed.
Strangler Fig Pattern: A Gradual Migration Approach
The Strangler Fig pattern is a strategy for migrating a monolithic application to a microservices architecture without causing significant downtime or disruption. It involves gradually introducing new microservices around the existing monolith, incrementally replacing its functionality.
In this approach, start by identifying suitable candidates within the monolith that can be extracted as independent microservices. These should be well-defined modules with distinct functionalities. Develop a new microservice to replicate the functionality of the chosen module. Gradually route traffic from the monolith to the new microservice. This can be done using techniques like feature flags, A/B testing, or canary releases. As more traffic is routed to the microservice, refactor the monolith to remove the corresponding functionality. Continue this process for other modules until the entire monolith is replaced by microservices.
How we did it: The diagram below outlines the various phases of transitioning from a monolithic to a microservices architecture. In our application, we followed this pattern to separate microservices based on subdomains. We utilized an API gateway to direct specific channel partners to the microservices. Based on the results, we gradually migrated additional channel partners and validated the process. Once we had sufficient QA and confidence, we routed all channel partners to the microservices.
Explore more here https://docs.aws.amazon.com/prescriptive-guidance/latest/modernization-decomposing-monoliths/strangler-fig.html
Branch by Abstraction
The branch by abstraction pattern is recommended if you’re looking to modernize components that are internally used in your application stack. This method allows you to modify the existing codebase so that the modernized version can coexist safely with the legacy version without causing any disruptions.
In this approach, Identify the monolithic component that needs to be modernized into a microservice. Create an abstraction layer that routes between the existing component and the new one. Once the abstraction layer is established, update existing clients to interact with it. Develop a new implementation of the abstraction with the reworked functionality outside the monolith. When the new implementation is ready, switch the abstraction to use it. After the new implementation fully meets user needs and the monolith is no longer in use, clean up the old implementation.
One key advantage is that it allows both the old and new versions of the code to coexist in production. This enables A/B testing, where you can route a portion of your users to the new system while keeping the rest on the existing one. This method provides a low-risk approach to live testing.
5. Infrastructure & Security
Use containerization tools like Docker to package microservices for consistent deployment across environments. Implement container orchestration with tools like Kubernetes to manage and scale services effectively. Use Continuous Integration/Continuous Deployment (CI/CD) to automate the build, test, and deployment processes to streamline microservices development. Implement an API gateway to manage traffic, authenticate requests, and handle cross-cutting concerns like rate limiting and security.
How we did it: We utilized AWS EKS, a cloud-native managed Kubernetes service. EKS handles many of the underlying infrastructure tasks, enabling our team to focus on application development. We configured automatic scaling rules to adjust the number of pods based on specific metrics, such as minimum and maximum pod counts and memory usage. To ensure high availability, we deployed the application across multiple zones to maintain business continuity.
The below diagram illustrates our first phase of deployment architecture where both monolithic and microservices coexist. To facilitate a gradual migration, we employed API gateway stage variables to route specific channel partners to the microservices. As we validated the results, we progressively migrated additional channel partners, ensuring thorough quality assurance before fully transitioning all traffic to the microservices.
Reference: https://kubernetes.io/docs/concepts/architecture/
Conclusion
Transitioning from a monolithic to a microservices architecture is a significant but essential step for organizations aiming to scale and innovate more rapidly. By breaking down your monolith into smaller, more manageable services, you can achieve greater flexibility, improved scalability, and faster development cycles. However, this transition requires careful planning, a deep understanding of your existing system, and a willingness to embrace new challenges. With the right approach, the rewards of microservices can far outweigh the complexities involved in the transition.
Here is our experience: We have been partnering with this client for five years, and this enterprise product has been live for the last four years. As more channel partners were on-boarded over time, the number of transactions increased significantly and started facing latency and HTTP 429 (too many requests) issues. We dedicated about a month to assess the pain points in the monolithic architecture and also explore the benefits of microservices compared to alternatives like Lambda functions and Dockerization of the entire application. During this period, we ran proof of concepts (POCs) and established clear objectives with the stakeholders, such as enabling independent evolution and autoscaling.
We then invested around a month in the design and decomposition phase, identifying domain boundaries and grouping the APIs. The development of the first set of microservices—Reservation, Locations, and Channel Partners—took approximately four sprints. We then updated the staging environment with microservices and let our channel partners test their integrations while we made more rigorous end-to-end integration tests. Following the confirmation, we successfully completed the first phase of our transition to microservices and moved into the next iteration of the ‘assessment and planning’ stage for the subsequent set of microservices.
We hope that our experience and journey will help your organization to successfully navigate the transition from monolithic to microservices architecture and unlock new levels of agility and efficiency.
Author
Sharan Kumar
System Architect