Converting Monolithic application to Micro-service | Monolithic to Micro-service Migration
If you’ve found your way to this page, chances are you’re familiar with the realms of grandpa’s monolithic app 🐔 and the grandson’s microservices architecture 🐣. I won’t bore you with explanations since you’ve already mastered these terms.
Disclaimer: Brace yourself; this blog might be a bit lengthy. But fear not, it’s a journey worth taking. Think of it like a roller-coaster ride — you hesitate at first, then as it kicks off, you’re terrified, and there’s no turning back. Yet, by the end, you find yourself wondering, “What on Earth was I scared of?” I assure you; you’ll echo the same sentiments by the conclusion of this blog… 😈
Enough with the formalities; let’s dive in 😋.
Step 1 : Creating a monolithic application
As we delve into the realm of microservices migration in this blog, let me provide a brief overview of the monolithic application that will be our focal point.
The application in question is a straightforward cricket app which deals with different types of cricket teams (ODI, T20, Test, etc.), each comprising a list of players. The relationship is many-to-many, meaning a single player can belong to multiple team types, fostering versatility across the teams. Few highlighting parts are :
- Controllers managing REST calls for both team and player entities, encompassing functions like addPlayer, getPlayer, getAllPlayers, addTeam, and getTeam.
2. Model classes designed for Object-Relational Mapping (ORM), including their Entity Transfer Objects (ETOs) and Data Transfer Objects (DTOs).
3. Repositories tasked with the actual data persistence.
4. Services offering abstraction, coupled with service implementations providing concrete logical implementations.
The dependencies utilized in crafting this monolithic app are :
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Please take a moment to review the monolithic app to ensure a solid grasp of the architecture. This will help us align our discussion more effectively. You can find the code at: https://github.com/thesiddharthraghuvanshi/Monolithic_to_Microservice_migration/tree/main/monolithic/cricket-app
All set…? YES/NO … 🤔
Absolutely sure? Of course, you are! 🤝
Now, let’s delve into the essence of what brought us here!
Step 2 : Divide & Conquer | Splitting the application to modules
I typically employ entity-based splitting, implying that a single microservice is tasked with handling all CRUD operations for a specific entity, while other functionalities are exposed to other microservices.
In the context of our cricket app, we’ll establish two micro-services: “team-service” and “player-service”, complemented by two additional ones, namely “service-registry” and “api-gateway”
(You have taken a seat to your ride now) 🎢
Let’s delve into the reasons behind these distinct applications, configure them, and explore the necessary adjustments for our monolithic application.
Step 3 : Creating player-service | micro-service 1
We will establish a distinct database, “players,” for this microservice, incorporating isolated controllers, models, services, repositories… etc which can be extracted from the existing monolithic app. Based upon our monolithic app, we’ll introduce two additional elements:
1- An extra dependency in pom.xml to configure and deploy it for high availability, with each server replicating state information about registered services to the others.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2- Introducing additional REST endpoints to expose specific data to “team-service,” such as getPlayersForTeam() and getPlayersFromPlayerIds(). This is necessary as the team service will no longer have direct access to the “players” database.
Step 4 : Creating team-service | micro-service 2
Similarly, for the “team-service,” we will establish a distinct database named “teams” and incorporate separate controllers, models, services, repositories… etc. Additionally, we will undertake three extra steps:
1- Include extra dependencies in pom.xml. First, as discussed in the “player-service,” and second, to communicate with the extra REST endpoints of “player-service,” namely getPlayersForTeam() and getPlayersFromPlayerIds().
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2- Create PlayerService.java with @FeignClient to facilitate communication with "player-service” -
@FeignClient("PLAYER-SERVICE")
public interface PlayerService {
@GetMapping("player/get/players")
public ResponseEntity<List<Integer>> getPlayersForTeam(@RequestParam Integer playersCount);
@PostMapping("player/get/players")
public ResponseEntity<List<PlayerEto>> getPlayersFromPlayerIds(@RequestBody List<Integer> playerIds);
}
3- In our main class, include @EnableFeignClients to enable component scanning for interfaces declaring themselves as Feign clients -
@SpringBootApplication
@EnableFeignClients
public class TeamServiceApplication {
public static void main(String[] args) {
SpringApplication.run(TeamServiceApplication.class, args);
}
}
Step 5 : Creating service-registry | micro-service 3
To facilitate communication between micro-services, we will establish a service registry that registers all instances of each micro-service, enabling communication between them. We utilize @EnableEurekaServer
to create this service registry -
@SpringBootApplication
@EnableEurekaServer
public class ServiceRegistryApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceRegistryApplication.class, args);
}
}
Additionally, include the following properties in the application.properties file-
eureka.instance.hostname=localhost
eureka.client.fetch-registry=false
eureka.client.registry-with-eureka=false
Step 5 : Creating api-gateway | micro-service 4
We’ll develop an API gateway to manage load balancing and routing for the micro-services we’ve created. To achieve this, add the following dependency in the pom.xml file and include properties in the application.properties file -
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway-mvc</artifactId>
</dependency>
spring.cloud.gateway.discovery.locator.enabled=true
spring.cloud.gateway.discovery.locator.lower-case-service-id=true
That’s it! You are all set now. The only thing left to cross-check is the names of the applications and the ports on which we are running all these four applications. Utilise the application.properties
file to configure these properties :
spring.application.name=application-name
server.port=port-number
Conclusion & Demo Project Link
In conclusion, our journey from a monolithic cricket app to a microservices architecture has been both enlightening and transformative:
- Entity-Based Splitting: We adopted the concept of entity-based splitting, wherein a micro-service exclusively manages CRUD operations for a particular entity. This approach ensures a clear separation of concerns and facilitates scalability.
- Microservices Architecture: The transition led us to the creation of two fundamental microservices — “team-service” and “player-service”. Accompanying them are “service-registry” and “api-gateway”, forming the backbone of our micro-services architecture.
- Database Separation: Both “team-service” and “player-service” now possess dedicated databases (“teams” and “players”, respectively), providing autonomy and encapsulation. Specialized controllers, models, services, and repositories enhance the modularity of each micro-service.
- Inter-Service Communication: Effective communication between micro-services is pivotal. We established a service registry using
@EnableEurekaServer
, enabling seamless interaction between instances of each microservice. Additionally, the creation of an API gateway ensures load balancing and efficient routing. - Configuration and Cross-Checking: The success of our micro-services architecture relies on meticulous configuration. Utilizing
application.properties
files, we configured dependencies, service names, and ports. It's crucial to cross-check these configurations to ensure a harmonious and well-functioning system.
In the end, this migration isn’t merely a technical evolution; it represents a shift in mindset, embracing modularity, scalability, and resilience. As you navigate through the intricacies of micro-services, remember the journey from a monolith to micro-services is a continuous learning experience, and the benefits reaped are well worth the effort.
DO NOT FORGET TO RUN YOUR APPLICATIONS — 😜
The entire git directory for this migration is at your disposal, Navigate through the readme.md
file :
Happy Coding !!! 😊