Application Modernization (Cloud OptimizedApplication)

Greetings, colleagues!

Over the past few years, I’ve had the privilege of working on several cloud modernization projects. Through these experiences, I’ve learned that the true power of the cloud lies in leveraging native services to their fullest potential.

It’s not enough to simply “lift and shift” applications to the cloud. To truly optimize for the cloud, we need to deeply understand our applications and map them to the equivalent cloud-native services available. Even cloud-native applications that were built recently may not be fully optimized for utilizing the latest and greatest cloud services.

In this post, I’d like to share a use case that illustrates how we can approach application modernization in the cloud. By carefully examining the application’s architecture, data requirements, and operational needs, we can identify opportunities to leverage cloud-native tools and services to improve scalability, reliability, and cost-efficiency.

I’ve captured the key steps and considerations in a detailed whitepaper. Please feel free to read through it, and don’t hesitate to reach out if you’d like to discuss your own cloud modernization use case. I’d be more than happy to share my insights and learn from your experiences as well.

Microservices Problem Statements

In the process of designing microservices, it is crucial to address the following challenges:

  1. Defining Microservice Boundaries: One challenge is determining the appropriate boundaries for each microservice. Careful consideration should be given to the nature of the application and its contextual boundaries, ensuring that each microservice has a well-defined responsibility.
  2. Data Retrieval across Microservices: Creating queries that retrieve data from multiple microservices can be complex. It requires careful planning and implementation of strategies such as data replication, event-driven architectures, or API gateways to facilitate efficient data retrieval across microservices.
  3. Achieving Consistency: Maintaining consistency across multiple microservices can be challenging. Ensuring data integrity, enforcing business rules, and handling distributed transactions are key considerations to achieve consistency in a distributed system.
  4. Designing Communication between Microservices: Establishing effective communication between microservices is vital. Choosing appropriate communication protocols, utilizing message queues, and defining clear interfaces and contracts enable seamless interaction and collaboration between microservices.

It is essential to address these problem statements and challenges when designing microservices. Furthermore, considering the principles of Domain-Driven Design (DDD) and identifying boundaries within the application’s domain can greatly assist in overcoming these challenges and creating a successful microservices architecture.

How to define the boundaries of each microservice

When defining the boundaries of each microservice, it is important to navigate the initial challenge of this task. Each microservice should function as an independent component of the application, embodying autonomy along with the associated advantages and considerations. So, how can these boundaries be identified?

To begin, direct attention towards the logical domain models of the application and the interconnected data. Seek out decoupled data entities and distinct contexts within the application. These contexts may exhibit varying business terminologies, essentially representing different business languages. It is crucial to establish and maintain independence for each context, enabling them to be defined and managed autonomously.

How to create queries that retrieve data from several microservices

A second challenge is how to implement queries that retrieve data from several microservices, while avoiding chatty communication to the microservices from remote client apps. An example could be a single screen from a mobile app that needs to show user information that’s owned by the basket, catalog, and user identity microservices. Another example would be a complex report involving many tables located in multiple microservices. The right solution depends on the complexity of the queries. But in any case, you’ll need a way to aggregate information if you want to improve the efficiency in the communications of your system. The most popular solutions are the following.

API Gateway. For simple data aggregation from multiple microservices that own different databases, the recommended approach is an aggregation microservice referred to as an API Gateway. However, you need to be careful about implementing this pattern, because it can be a choke point in your system, and it can violate the principle of microservice autonomy. To mitigate this possibility, you can have multiple fined-grained API Gateways each one focusing on a vertical “slice” or business area of the system. The API Gateway pattern is explained in more detail in the API Gateway section later.

GraphQL Federation One option to consider if your microservices are already using GraphQL is GraphQL Federation. Federation allows you to define “subgraphs” from other services and compose them into an aggregate “supergraph” that acts as a standalone schema.

CQRS with query/reads tables. Another solution for aggregating data from multiple microservices is the Materialized View pattern. In this approach, you generate, in advance (prepare denormalized data before the actual queries happen), a read-only table with the data that’s owned by multiple microservices. The table has a format suited to the client app’s needs.

How to achieve consistency across multiple microservices

As mentioned earlier, each microservice possesses private ownership of its data, exclusively accessible through its microservice API. Consequently, a significant challenge arises in ensuring end-to-end consistency across multiple microservices while implementing business processes.

To delve into this issue, let’s examine an example from the reference application. The Catalog microservice maintains comprehensive product information, including product prices. Conversely, the Basket microservice manages temporary data regarding product items added to users’ shopping baskets, which includes the price of each item at the time of addition. When a product’s price is updated in the catalog, it becomes necessary to synchronize the updated price across active baskets containing the same product. Additionally, the system should notify the user that the price of a particular item has changed since they added it to their basket.

Addressing this challenge involves establishing mechanisms for data consistency and communication between microservices. It requires implementing appropriate event-driven architectures, message passing techniques, or utilizing event sourcing and eventual consistency patterns. These approaches facilitate seamless updates and notifications across microservices, enabling efficient synchronization and ensuring a consistent user experience throughout the application.

How to design communication across microservice boundaries

Communicating across microservice boundaries is a real challenge. In this context, communication doesn’t refer to what protocol you should use (HTTP and REST, AMQP, messaging, and so on). Instead, it addresses what communication style you should use, and especially how coupled your microservices should be. Depending on the level of coupling, when failure occurs, the impact of that failure on your system will vary significantly.

In a distributed system like a microservices-based application, with so many artifacts moving around and with distributed services across many servers or hosts, components will eventually fail. Partial failure and even larger outages will occur, so you need to design your microservices and the communication across them considering the common risks in this type of distributed system.