Application Modernization With Microservices: Challenges and Best Practices
Image Source

Application Modernization With Microservices: Challenges and Best Practices

21 min read

Share

Managing outdated legacy systems may be challenging: accumulating technical debt hinders swift adaptation to market changes and user needs. Therefore, it’s crucial to find the most efficient way to modernize legacy applications. One common approach is implementing microservices architecture. While microservices offer significant advantages, adopting them can be difficult because of their complexity and the lack of a one-size-fits-all migration strategy.  

With over 17 years of experience in software development as a Solution Architect, I have led numerous modernization projects at MobiDev. My expertise in transforming outdated systems into agile, efficient, and secure infrastructures can equip you with valuable insights into this complex process.

In this guide for migration to microservices, I will explore common challenges when moving to microservices from monolithic architecture and outline the best practices developed by MobiDev, along with our clients’ success stories.

Microservices are a Tool, Not a Silver Bullet

Microservices have been the center of attention for years, but they aren’t a silver bullet solution for every project. Implementing microservices setup often involves compromises. However, businesses shouldn’t be discouraged from adopting microservices if it fits their goals. The most important thing is to set realistic expectations and assess whether the technology can meet the objectives.

Microservices are tailored for large-scale organizations deploying extensive systems. The industry often fails to clearly distinguish between different concepts, and “microservices” is one such term.

Microservices as an Architectural Practice: This involves breaking down applications into smaller, independent services.

Microservices as an Infrastructure Deployment Pattern: This focuses on how services are deployed and managed.

Service-Oriented Design: This can apply to both monolithic and distributed systems.

Distributed Monoliths: These are often mistakenly called microservices simply because they are distributed.

Think of microservices as a way for large organizations to manage and scale their systems efficiently. They enable small teams to maintain individual components and allow precise control over resource scaling. Dividing an application into small, self-contained services, each running  its own processes and communicating through APIs, has numerous advantages:

  1. Scalability and Flexibility: Microservices allow individual services to be scaled independently, making it easier to manage load balancing and system stability. They enable the use of different programming languages and frameworks for each service, providing flexibility in development.
  2. Fault Tolerance: If one service fails, it doesn’t affect the entire system. This fault isolation ensures higher system reliability.
  3. Efficiency and Cost Savings: Independent services can be developed, deployed, and maintained separately, leading to faster development cycles and reduced maintenance costs.
  4. Developer Productivity: Developers can focus on smaller modules, facilitating easier onboarding and parallel development by different teams.
  5. Testing and Debugging: Testing microservices individually is simpler than testing a monolithic system, reducing complexity and improving debugging processes.

The disadvantages of microservices architecture aren’t as obvious. 

  1. Management Complexity: Handling multiple services can be challenging, especially for teams new to microservices. Ensuring compatibility and monitoring interactions require significant effort. 
  2. Network Overheads: Microservices rely on network communication, which can introduce latency and increase traffic, complicating error tracking.
  3. Consistency Challenges: Maintaining data consistency across independent services can be difficult, and competent microservices implementation requires a strong DevOps team for deployment and management.

To make it more illustrative, let’s take as an example the story of Shopify, an e-commerce platform for online stores and retail point-of-sale systems and their approach to system modernization. 

Shopify: Migrating to a microservices architecture  

Shopify, with its massive Ruby on Rails codebase developed over a decade by thousands of developers, initially operated as a monolith. This meant all functionalities, from billing to shipping, were tightly interwoven in a single codebase. While this approach worked for many years, it eventually led to issues with scalability, code fragility, and developer productivity.

As microservices gained popularity, Shopify considered them but recognized their own set of challenges, such as increased complexity in managing multiple services. Instead, the company chose to evolve into a modular monolith, keeping the codebase unified but enforcing strict boundaries between different components. This approach retained the simplicity of a monolith while introducing the modularity needed for scaling.

By reorganizing the code according to real-world concepts (like orders, shipping, and billing) and defining clear interfaces between components, Shopify significantly improved developer productivity and system maintainability. This modular monolith strategy allowed them to address scaling issues without the overhead and complexity of a full microservices architecture. As I’ve already mentioned in the article, modernizing with microservices is not always the best solution. Let’s investigate when to use and not to use microservices.    

When to Modernize With Microservices 

Moving to microservices is one of the sought-after app modernization techniques. Starting a product directly with microservices might not be the optimal approach. Instead, one should consider beginning with a monolith or placing the core business logic within it. From here, extracting services becomes more manageable. Efforts to achieve perfectly isolated microservices might be overambitious, potentially leading to unnecessary complications.

For projects involving large teams, microservices can be the right choice. 

In the table below, I have gathered the most common cases when migrating to microservices could be the right decision and when this approach is not the best architecture solution.  

If you’re still not sure what architecture solution fits best with your product, you can always consult with me and my colleagues. Based on your business goals we’ll find the best way to modernize your product to meet your needs.

Need Tech Consulting?

Book a call

Monolith to Microservices Migration Guide 

If you still choose to move with microservices, it’s crucial to be aware of what it takes to implement them.  A successful migration to microservices requires in-depth infrastructure assessment, decomposition of your monolith architecture, software refactoring, and thorough testing. Let’s get straight to what can make the process of moving from a monolith to microservices as seamless as possible and start with the main five steps this transformation involves.  

Step 1. Conduct software audit 

Ensuring a successful migration to microservices begins by justifying the shift from a business perspective. Since every project possesses unique technical capabilities and limitations, it’s vital to consider that the move impacts the entire product architecture, which needs to be effective for future growth. It’s thus advisable to collaborate with both a business analyst and a technical expert to accurately evaluate current system requirements and draft an efficient modernization roadmap.

Before moving to microservices, a comprehensive software audit is imperative. This audit should pinpoint the current technology underpinning the product. It’s crucial to discern if this technology might pose limitations for future endeavors or if an alternative should be adopted for the microservices. Engaging with experts familiar with the particular technology can provide invaluable insights, ensuring a smooth and efficient architectural transformation.

Step 2. Identify Parts of the System to be Transferred to Microservices 

The next step is to understand what parts of the system can be transitioned to microservices and plan how these microservices will interact (dependencies will describe how changes in one module affect other modules). Essentially, it’s about designing the product’s overarching architecture and prioritizing which services to extract.

Microservices are most effective when they handle specific functionalities. Examples include real-time communication, data processing pipelines, background job processing, or interactions with external services. These functionalities might need data access from the monolith but are technologically distinct.

For instance, in a web system where users can upload images that require resizing and storage, these specific processes could be managed by a microservice since they perform specialized tasks. One advantage of this approach is that if performance issues arise, it’s easier to scale just that particular microservice rather than the entire monolithic system. Dismantling the whole system into microservices isn’t always justified, as it can demand substantial effort for subsequent synchronization. It’s crucial to focus on functionalities that make sense to externalize during the analysis phase.

Step 3. Break The Monolith to Microservices

Breaking a monolith into microservices can be done by applying two approaches. 

In the first one, we start by separating the desired functionality within the monolith and gradually disconnect it from other parts, but keep it within the monolith. After disconnecting all ties and designing a new API, we move it out to launch as a separate microservice. This approach requires significant changes in the monolith during the process.

The second approach includes making an independent copy of the desired functionality and developing it into a microservice, while the original still runs in the monolith. Once the new microservice is fully functional and tested, we remove the original from the monolith.

Step 4. Handle Data Management 

A key principle of microservices architecture is that each microservice should have its own dedicated database. Yet, breaking up a monolithic database can be challenging due to potential overlaps and interdependencies between database objects.

Different microservices can use distinct databases, programming languages, and data storage solutions. Working with certain databases may be more complex than others, making it impractical to consolidate all data into a single database. Hence, specialized storage systems are often used for specific data types.

Data management operations for microservices can be grouped according to the involved data-related patterns:

 

1. Database-per-Service

The Database-per-Service pattern is a pivotal approach in microservice architecture, underscoring the importance of autonomy and encapsulation. By assigning a dedicated database to each microservice, it ensures data consistency and isolation, and reduces contention among services. 

However, data integration and cross-service querying can become complex, requiring efficient communication protocols and well-defined interfaces. Additionally, database schema evolutions must be handled with care to avoid unintended service disruptions. But, with the right strategies in place, the Database-per-Service pattern can profoundly enhance scalability and fault tolerance in microservice ecosystems.

2. Saga 

The Saga pattern is a critical strategy in microservice transactions, addressing the challenges of maintaining data consistency across distributed systems. Instead of relying on traditional database transactions, it breaks operations into a series of isolated, compensatable actions. If a step fails, compensating transactions are triggered to ensure system consistency. 

While this decentralized approach bolsters system resilience and scalability, it demands careful orchestration and error handling to effectively manage potential failures.

3. API composition

The API Composition pattern is a foundational method within microservices architecture that manages the challenge of data retrieval from multiple services. In an environment where each microservice is responsible for its unique piece of data, direct client-side data querying can be both complex and inefficient. 

The API Composition pattern addresses this by using an intermediary – often called an API composer or aggregator – to assemble the required data from various microservices into a unified response. By doing so, it provides clients with a singular access point, simplifying queries and streamlining data delivery. While this approach enhances the client experience and reduces direct microservice interactions, developers must ensure the composer remains efficient and doesn’t become a bottleneck or a single point of failure.

4. CQRS

Given that microservices advocate for decentralized data management, complexities can arise, especially when one service needs to both update data and query it. 

CQRS addresses this by segregating the responsibility of command operations (writes) from query operations (reads). In a microservices environment, this means a service can be optimized for its most common task: some services might be read-heavy, while others deal primarily with write operations. As a result, each can scale independently based on its workload. 

CQRS in microservices offers many advantages, but introduces additional complexity, particularly in ensuring data consistency across services. Thus, its adoption should be thoughtfully considered within the context of the system’s specific requirements.

4. Event sourcing 

Event Sourcing emphasizes the capture and storage of all changes to the application state as a sequence of events. Instead of storing the current state of data in a domain, it stores a series of state transitions, enabling a system to reconstruct its state by playing back the events.

In the context of microservices, this allows each service to maintain its own history, promoting autonomy and decoupling between services. As events become the single source of truth, they can be leveraged for multiple purposes, from analytics to audit trails. 

Additionally, this approach simplifies error handling and recovery mechanisms, as one can revert to a previous state or reprocess events. While powerful, the pattern necessitates careful consideration of event versioning and storage scalability.

5. Shared Database

The Shared Database anti-pattern arises when multiple microservices or components in an architecture interact directly with a common database instead of through APIs or messaging. This direct access not only compromises the autonomy of services but also introduces potential data integrity concerns. 

A system built on this approach may face security challenges, as services can inadvertently expose sensitive data. Such a design also complicates system evolution, turning seemingly minor database changes into complex tasks that demand coordination across multiple services.

Next Steps

Whatever data management strategy is chosen, you need to avoid a Distributed Monolith. If services cannot be fully isolated, it often leads to more issues. Such a solution remains monolithic in terms of structure and databases. Even though it seems we have separated them, they end up having many connections, and most of the advantages of microservices are lost.

Next, we need to develop API interfaces through which the microservice will communicate with the monolith and other microservices. The referenced service provides data via an API that the requesting service requires.

Step 4. Ensure Interservice Communication

When designing interservices communication, consider the interaction process. Service interactions can be classified into two groups:

  • One service processes each client request (one‑to‑one interaction)
  • Each request is processed by a number of services (one‑to‑many interactions)

Also, take into account the synchronous or asynchronous nature of the interaction:

  • Synchronous: When the client expects an immediate response from the service, it may become blocked while awaiting this response. This is direct, similar to when they call each other synchronously.
  • Asynchronous: In this case, the client doesn’t get blocked waiting for feedback. The response, if provided, might not be dispatched right away. This is similar to when they utilize a message broker. There’s a separate software with data channels. A service sends a notification there, and the services needing this information subscribe to it. They can then process this data asynchronously when possible.

If, from a business logic perspective, something can be done asynchronously, it’s better to do it that way. This ensures greater stability and facilitates load balancing.

Step 5. Test and Deploy

Testing microservices also has some specifics. In traditional monolithic structures, the entire application could be tested as a cohesive unit. In contrast, a microservices-based application may encompass numerous services that might not always be available for simultaneous testing. This interdependence makes end-to-end testing complex and demands alternative testing methods.

There are specific testing types tailored to microservices:

  • Unit testing, which focuses on individual service behavior
  • Integration testing that ensures harmonious inter-service operation
  • Performance testing for assessing system responsiveness
  • Component testing for individual service functionalities
  • Contract testing to examine user-service interactions
  • End-to-end testing for holistic application performance. 

Each testing type ensures that microservices function individually and collectively to meet business objectives. Yet, testing microservices is not without its challenges. 

For instance, errors in one microservice can set off a chain reaction, complicating root cause analysis. With microservices often communicating through various channels and protocols, it demands specialized skills and knowledge. Furthermore, the necessity of testing multiple endpoints, coupled with the imperative for automated testing, requires proficiency in script writing and automated testing tools.

MobiDev’s approach to testing microservices encompasses meticulous individual API tests for every microservice, extensive integration testing for inter-service communication, and comprehensive end-to-end testing via APIs and user interfaces.    

A microservices-based application fairly often is a system that comprises a simple user interface and a complicated backend under the hood. A QA engineer, who tests microservices, must be experienced in using Docker and console utilities for gathering logs and connecting to containers. Being skilled in programming is a plus.

Andrii Makaiev

QA Engineer's Team Leader

Challenges and Best Practices of Microservices Development

Microservices development requires the whole set of approaches to tackle typical challenges.  Awareness of these difficulties allows you to level them out at the early stages and prepare the environment for the successful implementation of microservices.

Data Consistency

Ensuring accurate transactions and data consistency across different services can be challenging. While there’s no one-size-fits-all solution, there are some general guidelines for managing data within a microservices structure. 

For areas demanding robust consistency, one service can be the primary source of truth for a specific entity. Other services can access this primary source via an API. Some services might maintain their own version of the data or a portion of it. This data can be consistent with the primary data but isn’t viewed as the main source. 

For instance, in an e-commerce system, there might be a customer order service and a recommendation service. While the recommendation service could be privy to the order service’s events, in cases like a customer seeking a refund, it’s the order service that retains the comprehensive transaction record.

Experienced developers can select the most suitable approaches to ensure data consistency, considering the nuances of the specific case. This is because there’s no universal way to achieve this seamlessly.

Team Organization

While developing microservices, each team can operate with its own management, methodology, etc. Therefore, it’s essential to have a clear structure for synchronization and communication between teams. This is especially relevant if you plan to split the workload between in-house and outsourced engineers.

The problem with highly formalized structures is that teams might efficiently create their own service, but if they don’t consider how other teams use that service, it can happen that the data provided by these services come in different formats, etc. Hence, it’s necessary for the project to have a role of an optimizer who coordinates the actions of the teams. 

Basically, we need to employ Conway’s Law here, which states that architecture of software systems is similar to the communication structure of teams working on them. Therefore, if you want to create an architecture of autonomous services, you should first organize small autonomous development teams that can interact with each other effectively.

DevOps Efforts

Working with microservices requires DevOps efforts, as the infrastructure is more complex. With microservices, deployments must be coordinated to ensure all versions of the microservices can interact seamlessly, given they are all interconnected and might have backward incompatible changes. It’s crucial to prepare all dependencies in advance and employ flexible tooling (like Kubernetes and Docker). DevOps engineers can help to launch and deploy this infrastructure. 

Microservice architecture troubleshooting also becomes more challenging as pinpointing the root cause of an error is tougher. In a monolithic structure, this process is relatively straightforward. With microservices, it’s more complicated, since one service might receive data and then pass it to another, making it harder and more time-consuming to determine where in the chain things broke down. To address this, a centralized log aggregator, a deployment orchestration system, and a separate distributed tracing system need to be integrated, making the entire setup more manageable. 

The distributed nature of microservices inherently presents more opportunities for attack vectors. This demands a distinct security approach compared to monolithic structures, highlighting the aptness of DevSecOps. Leveraging automation tools streamlines DevOps processes, enabling seamless Continuous Integration and Continuous Delivery (CI/CD).

Anton Lohvynenko

Solution Architect

Now that I’m done with the theoretical part, let me share with you MobiDev’s experience in implementing a microservice architecture for our clients.

Success Story: Breaking a PHP-based Monolithic App to Microservices Using Ruby on Rails

Client Overview: GrowthHackers is an influential player in the online marketing realm, providing a platform for marketers that fosters community engagement and offers essential tools. Their key challenge was the need to expedite product development and overhaul their prevailing digital ecosystem.

Project Description: GrowthHackers had two cornerstone products: a communication platform and software to help teams achieve their growth objectives.

When diving into the technological aspects, a plethora of technologies were at play, from Angular and Ruby on Rails to PHP and WordPress. Third-party integrations enriched the system further, integrating platforms such as Twitter, Jira, Slack, and Stripe. Deployment and hosting were addressed using a combination of powerful solutions including Docker, AWS, and Heroku.

Project Milestones:

  • From Monolith to Microservices: The primary task was to transcend the constraints of the WordPress framework. The solution was a shift from a monolithic architecture, recasting the communication platform using Ruby on Rails (RoR). Potential performance bottlenecks were preemptively tackled using smart caching strategies complemented by Redis.
  • Elevating the System with a Focus on Growth & Automation: The system’s growing repertoire of features necessitated enhanced capabilities. Docker was employed to streamline microservice interactions, while a foundation was set using Heroku and PostgreSQL. The incorporation of Trailblazer allowed for a domain-driven design on RoR, segmenting the business logic into distinct entities.
  • Championing Product Evolution: Product releases were intense, occurring twice weekly, managed by a peak team of 12. This dynamic workflow was efficiently overseen, with a keen eye on maintaining clarity with the client. By handling the day-to-day, the client was free to channel their energy into fostering business growth.

Outcome: GrowthHackers’ decisive shift to microservices, supported by a selection of advanced tools, set the stage for a modern, scalable, and high-performance platform, priming them for continued success and expansion.

Looking for an awesome offshore development team? Check out MobiDev. I’ve been working with them for years.

CEO of GrowthHackers

Sean Ellis

CEO, GrowthHackers

How MobiDev Can Help You Modernize Your App with Microservices 

As a representative of MobiDev, I am proud of the expertise we have gained in the area of software transformation. Our seasoned team includes competent engineers, business analysts, meticulous QA engineers, and proficient DevOps specialists, all primed to facilitate a seamless and secure shift from a monolithic to a microservices architecture for your product.

With over 14 years of industry leadership, MobiDev has refined a development paradigm that places paramount importance on security at every juncture. We’re flexible in our approach. Whether you’re looking for a dedicated team that can take on the whole task of migrating your app with microservices or need seasoned engineers & architects to extend your in-house team, we are here to help. 

Feel free to check out our application modernization services or contact us directly. My colleagues and I will be happy to assist you in upgrading your product’s architecture with consideration of existing technical and business limitations.

Contents
Open Contents
Contents

GET IN TOUCH

Whether you want to develop a new product or update an existing one, we're eager to assist. Call us or fill in the form via CONTACT US.

+1 916 243 0946 (USA/Canada)

CONTACT US

YOU CAN ALSO READ

Modernizing Legacy Applications in PHP Challenges and Approaches

PHP Applications Modernization: Challenges and Approach…

Practical Guide to Modern Web App Development Technologies

Practical Guide to Using the Latest Technologies in Web…

Ruby On Rails App Update: The Product Owner’s Complete Guide

Ruby On Rails App Update: The Product Owner’s Complet…

We will answer you within one business day