Developers frequently work with monolithic applications. However, the popularity of these monolithic applications has waned due to their lack of flexibility. Scaling a specific component requires scaling the entire application.
Transitioning to a microservice architecture, with modular, independently scalable units addresses these challenges and is well-suited for cloud environments. This article focuses on moving a monolithic application, exemplified by a Java™-based e-commerce app called Pedal, to the cloud, offering guidance for this complex task.
Modernization explained with an example application
Modernization is a complex process that can vary depending on the organization's context, architecture, and needs. To simplify, let's consider an example application called Pedal, a fictional e-bike company known for creating high-quality, stylish, and performance-driven electric bicycles.
View source for Pedal monolithic applicaiton
Pedal currently has a monolithic architecture where all the components, from database interaction to business logic to the user interface (UI), are tightly coupled and operate as a whole. However, to meet its growing needs, Pedal wants to modernize its application for greater scalability and resilience, particularly by leveraging cloud-based solutions.
One approach for modernizing Pedal's application is to adopt a microservices architecture. This involves breaking down the monolith into separate services that manage distinct functions, such as user management, bike listings, order processing, and employee interactions. These microservices are designed to be loosely coupled and independently deployed, communicating with each other using methods like HTTP and REST, often paired with JSON for data exchange.
Additionally, Pedal plans to adopt a hybrid cloud approach. This means deploying some services on-premise while staying connected to others on the public cloud and third-party public cloud services. This approach allows Pedal to leverage the scalability and flexibility of the cloud while maintaining certain services on-premise for regulatory compliance or other reasons.
Why use a hybrid cloud approach?
Adopting a hybrid cloud setup provides businesses with more deployment options and flexibility in choosing where their applications and data reside. For example, critical applications can be kept on-premise while lower-risk services can be migrated to a public cloud. This approach helps in managing sensitive data within a private cloud or on-premise infrastructure to protect it from potential vulnerabilities associated with public clouds. Meanwhile, using public cloud resources for less sensitive data minimizes the cost of purchasing and maintaining additional on-premise infrastructure.
For an application like Pedal, a hybrid cloud approach offers the best of both worlds:
Data Management
A private cloud can host Pedal's sensitive user and employee data, while the public cloud can host less sensitive, more extensive data such as bike listings or images. However, it's important to ensure that the cloud platforms offer encryption and other security measures to handle sensitive user data in compliance with industry regulations.
Scalability
Scaling individual services becomes more efficient when running high-demand services like bike listings or order processing. Pedal can optimize resource usage and significantly reduce costs by scaling only the necessary components. In contrast, with its current monolithic architecture, the company must regularly scale its entire application, even if only a specific part faces increased demand.
Safe testing and deployment
As Pedal introduces new features, such as virtual test rides or augmented reality views of bikes, the public cloud can serve as a sandbox for testing and deployment.
The journey to modernize a traditional Java monolith includes the following steps:
- Refactoring a monolith into microservices
- Implementing cloud-native design patterns for Pedal
- Refactoring and decoupling databases
- Deploying the application
Refactoring a monolith into microservices
At a high level, converting a monolith into microservices involves several steps, such as:
Step | Title | Description |
1 | Assessing and planning | Evaluate the existing monolithic application to identify components suitable for microservice architecture. |
2 | Decomposing the application | Break down the application into smaller, independent services using functional decomposition. |
3 | Adopting a microservices framework | Use Quarkus to create efficient, lightweight microservices suitable for cloud-native environments. |
4 | Defining service boundaries | Determine the boundaries of each microservice, ensuring they are loosely coupled and can operate independently. |
5 | Restructuring data | Reorganize the data architecture to support distributed systems, possibly implementing separate databases for different services. |
6 | Updating communication mechanisms | The transition from in-process method calls to inter-service communication, such as REST APIs or messaging queues. |
Pedal must transition from traditional structures to a microservices-based model to fully leverage the benefits of a hybrid cloud architecture, akin to transforming a bustling city into a network of small, efficient towns. This transition involves functional decomposition — breaking down complex systems into granular functions for independent management and scalability.
Quarkus plays a pivotal role in this transition. As a Java framework, Quarkus is well-suited for creating microservices and cloud-native applications due to its efficiency in reducing Java applications' footprint and startup time. Given Pedal's Java-based infrastructure, incorporating Quarkus in the refactoring process can facilitate a smooth transition to microservices, enhancing runtime efficiency, and streamlining development.
Practically, these steps can be completed through a series of hands-on tasks for refactoring a monolith into microservices, using Pedal as an ongoing example.
Identify domain boundaries
Identifying domain boundaries begins with understanding the core functionalities within the monolithic application. For Pedal, this involves identifying distinct functions such as user management, bike listings, order processing, and employee interactions.
Isolate the database
In a monolith, data is often deeply interconnected. Pedal's PostgreSQL and SQL Server databases may be cross-referenced, so the goal is to compartmentalize this data. Ideally, each microservice has its own database, ensuring independence. You can split the monolithic database into smaller, service-specific databases: a separate database or schema for user profiles and histories, a distinct data model for bike inventories, and another for managing orders, for example.
Isolate and extract functional modules
The next step is to manually isolate and extract the code modules. For example, the bike listing feature within the e-bike directory might evolve into a microservice with its own codebase. Similarly, you can extract the user management capabilities and place them in another dedicated service.
Build the API gateway
Communication between microservices is essential. In Pedal's monolith, these components could interact directly, but in a microservices architecture, they must rely on APIs. The API gateway acts as the mediator between the client and individual microservices, routing client requests to the appropriate service and collating the responses.
For example, the order processing microservice fetches bike details from the bike listing microservice, while the user management service verifies order histories from the order processing service. These interactions can be facilitated using RESTful APIs, message brokers, or other communication mechanisms.
Implement microservice infrastructure
Once you've decomposed the services, they'll require an environment. Consider using a container orchestration platform such as Kubernetes to coordinate these services across environments.
In a hybrid cloud environment, you can dynamically locate services, which necessitates setting up a service discovery mechanism to track them. Next, implement continuous integration/continuous delivery (CI/CD) pipelines for individual microservices to ensure independent deployments.
Test each microservice
With Pedal's functionalities now decomposed, rigorous testing is crucial. Before going live, test each microservice independently and then as part of the integrated system. This approach ensures that the microservices function correctly in isolation and when interacting with other services.
Cloud-native design patterns
Several design patterns have emerged to support cloud-native design. Pedal could benefit from one of the two popular patterns below:
The stateless design pattern
The stateless design pattern ensures that the application does not rely on the server to retain information about its state between requests.. Each request contains all the necessary data, accommodating horizontal scaling and boosting resilience against failures.
When a user interacts with the Pedal application to view bike details or place an order, the request must carry all the necessary data. Avoid storing data about the user’s session in the application after processing the request. If user authentication is required, stateless authentication mechanisms, such as a JSON web token (JWT), can help.
The dependency injection pattern
With the dependency injection pattern, components or objects receive their dependencies from the outside rather than creating them internally. It’s a way to invert control of dependency creation: Instead of the component, an external entity handles it.
Implementing this pattern in Pedal is straightforward using a framework such as Spring Boot. Annotations like @Service and @Repository can annotate components.
When one component requires another, Spring Boot can inject it using the @Autowired annotation. For instance, if Pedal has a BikeService that depends on a BikeRepository, instead of manually creating an instance of BikeRepository in BikeService, Spring Boot can provide it automatically.
Database refactoring and decoupling
Database refactoring involves making structural changes to improve the database design while preserving its behavioral and informational semantics.
A crucial step to transform Pedal, a Java-based monolithic application, into a microservices architecture is refactoring and decoupling its database systems. Originally, Pedal used a PostgreSQL database for user data and an SQL database for employee data. To modernize the architecture, each microservice gets independent database access, aligning data storage with specific service needs.
For example, the user management service now interacts solely with the PostgreSQL database, enhancing efficiency and scalability. This targeted approach allows for more agile responses to changing demands, like scaling the order processing service independently, to improve performance and cost-effectiveness.
The following sections outline how to decouple Pedal’s database services.
Identify storage needs
First, it’s essential to understand the components’ data types and storage needs.
The core functionality, like bike orders or user details, is transactional in nature. Transactional data requires atomicity, consistency, isolation, and durability (ACID) guarantees, so relational databases like PostgreSQL or Microsoft SQL Server are suitable.
On the other hand, features like bike reviews, user feedback, or perhaps cached listings for faster access might not require the strict guarantees of a relational database. For this type of non-transactional data, NoSQL databases like MongoDB or Apache Cassandra are good options.
Data decoupling by service
The next step is to decouple the data by service. The bike listing service might rely solely on transactional data, such as details of available bikes, their specifications, and prices. You can store this data in PostgreSQL.
If Pedal introduces a feature that captures user reviews and bike feedback, you can store this non-transactional data in a NoSQL database like MongoDB.
Service-database mapping
Next, ensure each microservice only interacts with its associated database or databases. The bike listing service should only have access and permissions to interact with its PostgreSQL schema. The user feedback service should only be able to interact with its NoSQL database.
Synchronization and event-driven updates
In a decoupled environment, one service might require information about changes in another service. If a new bike gets added to the listing, affecting an ongoing promotional campaign managed by another service, you can achieve the sync using an event-driven model. Employ tools like Apache Kafka or RabbitMQ to help broadcast these events.
Deploying the applications
Once you’ve refactored Pedal’s application and decoupled its database, deploying its services is next.
When it comes to the deployment of a hybrid cloud application, two concepts are key: containerization and orchestration. Containerization is the process of packaging an application with its dependencies, libraries, and binaries, ensuring consistency across different environments. Orchestration, on the other hand, means managing these containers to ensure they interact seamlessly and scale effectively.
Containerizing Pedal using Podman
Podman, a powerful, open-source g tool that lets you manage containers without a daemon, is a perfect fit for your refactored Pedal application. It features a Docker-compatible command-line front end that acts as a drop-in replacement for Docker.
Here’s how to containerize Pedal services using Podman. For each of Pedal’s refactored microservices, create a Dockerfile that describes the environment for that service. Next, build the Docker image using Podman. Then, run it locally to ensure everything’s working as expected. Finally, push the image to a container registry.
Learn more about Pedal’s containerization journey
Deploying Pedal on Red Hat OpenShift
Red Hat® OpenShift®, an enterprise Kubernetes platform, streamlines the deployment, scaling, and management of applications. To deploy Pedal’s containers on OpenShift, first, create a new project in OpenShift dedicated to Pedal. This project provides an isolated environment for all Pedal-related resources.
Before OpenShift can deploy the container, the image must be available in a container registry. Push the Podman-created image to a registry using podman push. Next, in OpenShift, use the
“Deploy Image” option. Here, provide the URL of the pushed image in the registry. OpenShift will pull this image and use it to create the necessary deployment configurations.
Once you’ve deployed the application, create a service in OpenShift to expose the required ports. Then, create a route to expose this service to the outside world, making the Pedal application accessible to users. With OpenShift’s dashboard, you can monitor the health of your application, scale it up or down based on demand, and manage many other lifecycle aspects.
Learn more about Pedal’s deployment journey
Next steps
Straightforward, monolithic architecture can become cumbersome to scale and maintain. As the system grows, it creates bottlenecks, reducing agility and slowing innovation. By decomposing functionalities into smaller, independent services, you can increase the flexibility, scalability, and resilience of your applications. That’s why many organizations are making the transformative journey from monolithic architectures to distributed, cloud-native systems.
By combining on-premise infrastructure with cloud resources, hybrid cloud environments provide the flexibility that modern applications require, with rapid scaling, cost optimization, and improved resilience. With tools like Podman for containerization and platforms like OpenShift for orchestration, deploying and managing applications becomes streamlined and consistent.
Check out more Pedal based guides at https://github.com/redhat-developer-demos/?q=pedal&type=all&language=&sort=
Last updated: April 29, 2024