DEV Community

Cover image for From Monolith to Microservices: Real-World Case Studies and Lessons Learned
joswellahwasike
joswellahwasike

Posted on

From Monolith to Microservices: Real-World Case Studies and Lessons Learned

Shifting from a monolithic architecture to microservices can be challenging, but numerous companies have successfully navigated this transformation, gaining significant benefits in scalability, flexibility, and maintainability. In this article, we'll explore real-world examples of companies that have made this transition. We will delve into the obstacles they encountered, the strategies they implemented, and the lessons they learned. To enhance understanding, coding examples will be provided, demonstrating key concepts that can be practically applied.

Understanding Monolithic and Microservices Architectures

  • Monolithic Architecture

This approach involves developing an application as a single, tightly coupled unit. While it simplifies early development and deployment, it can lead to scalability issues and development bottlenecks as the application grows.

  • Microservices Architecture

In contrast, this architecture breaks down an application into smaller, loosely coupled services, each responsible for a specific function. This approach allows for independent development, deployment, and scaling, making the system more adaptable and easier to manage.

Case Study 1: Netflix

  • Overview

Netflix serves as a prime example of a successful transition from a monolithic architecture to microservices. The company faced significant scalability and reliability challenges as its user base expanded.

  • Challenges

Scalability: The monolithic system struggled to support the growing number of users and simultaneous streams.

Development Bottlenecks: The expanding codebase became increasingly difficult to manage, slowing down development cycles.

Operational Overheads: Deploying new features and fixes was complex and risky.

  • Strategies Incremental Migration: Netflix started migrating non-critical services first, allowing them to refine their strategies with minimal disruption.

Automated Testing and Deployment: Emphasizing automation in testing and CI/CD pipelines ensured reliability and speed.

Service Registry and Discovery: Tools like Eureka were used for service discovery, facilitating communication between microservices.

  • Coding Example: Basic Microservice with Spring Boot
  • Setting Up the Project
  1. Create a new Spring Boot project:

. Use Spring Initializr to generate a new project.
. Select dependencies: Spring Web, Spring Boot DevTools, and Spring Boot Actuator.

  1. Open the project in VS Code:

. Unzip the downloaded project and open it in VS Code.

  1. Define the service:

. Open src/main/java/com/example/demo/DemoApplication.java and replace its contents with the following code:


package com. example.demo;

import org. spring framework. boot.SpringApplication;
import org. spring framework.boot.autoconfigure.SpringBootApplication;
import org. spring framework.web.bind.annotation.GetMapping;
import org. spring framework.web.bind.annotation.RestController;

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
 }

 @RestController
    class HelloController {
 @GetMapping("/hello")
        public String sayHello() {
            return "Hello, Netflix!";
 }
 }
}
Enter fullscreen mode Exit fullscreen mode
  1. Run the application:

. In the terminal, run ./mvnw spring-boot: run.

  1. Test the service:

. Open a web browser and navigate to http://localhost:8080/hello. You should see "Hello, Netflix!".

Lessons Learned

. Gradual Transition: Incremental migration reduces risks and allows for iterative improvements.

. Automation is Key: Automated testing and deployment processes are crucial for maintaining reliability.

. Monitoring and Observability: Enhanced monitoring and observability are necessary to manage the complexity of microservices.

Case Study 2: Amazon

  • Overview
    Amazon transitioned from a monolithic architecture to microservices to support its growing online marketplace.

  • Challenges

. Scalability: The monolithic system couldn't efficiently support the increasing number of customers and services.

. Deployment Risks: Updates had to be rolled out across the entire application, increasing the risk of system-wide failures.

. Limited Flexibility: Adding new features was cumbersome due to tightly coupled components.
Strategies

. Decoupling Services: Amazon decomposed its monolithic application into smaller, independent services.

. Two-Pizza Teams: Adopted the "two-pizza team" concept to keep teams agile and autonomous.

. Event-Driven Architecture: Used an event-driven approach to maintain decoupled services.

  • Coding Example: Event-Driven Microservice with Kafka
  • Setting Up the Producer Service
  1. Create a new Spring Boot project:

. Use Spring Initializr to generate a new project.
. Select dependencies: Spring Web, Spring for Apache Kafka, and Spring Boot DevTools.

  1. Open the project in VS Code:

. Unzip the downloaded project and open it in VS Code.

  1. Define the producer service:

  2. Open src/main/java/com/example/kafka/KafkaProducerApplication.java and replace its contents with the following code:


package com. example.Kafka;

import org. spring framework. boot.SpringApplication;
import org. spring framework.boot.autoconfigure.SpringBootApplication;
import org.springframework.kafka.core.KafkaTemplate;
import org. spring framework.web.bind.annotation.GetMapping;
import org. spring framework.web.bind.annotation.RequestParam;
import org. spring framework.web.bind.annotation.RestController;

@SpringBootApplication
public class KafkaProducerApplication {

    public static void main(String[] args) {
        SpringApplication.run(KafkaProducerApplication.class, args);
 }

 @RestController
    class ProducerController {
        private final KafkaTemplate<String, String> kafkaTemplate;

        ProducerController(KafkaTemplate<String, String> kafkaTemplate) {
            this.kafkaTemplate = kafkaTemplate;
 }

 @GetMapping("/send")
        public String sendMessage(@RequestParam("message") String message) {
            kafkaTemplate.send("testTopic", message);
            return "Message sent: " + message;
 }
 }
}
Enter fullscreen mode Exit fullscreen mode
  1. Set up Kafka:

. Download and install Apache Kafka from the official website.
. Start Kafka and Zookeeper using the following commands in separate terminals:
sh

Start Zookeeper

bin/zookeeper-server-start.sh config/zookeeper.properties

Start Kafka

bin/kafka-server-start.sh config/server.properties

  1. Run the producer application:

. In the VS Code terminal, run ./mvnw spring-boot: run.

Setting Up the Consumer Service

  1. Create a new Spring Boot project:

. Use Spring Initializr to generate a new project.

. Select dependencies: Spring for Apache Kafka, Spring Boot DevTools.

  1. Open the project in VS Code:

. Unzip the downloaded project and open it in VS Code.

  1. Define the consumer service:

. Open src/main/java/com/example/kafka/KafkaConsumerApplication.java and replace its contents with the following code:


package com. example.Kafka;

import org. spring framework. boot.SpringApplication;
import org. spring framework.boot.autoconfigure.SpringBootApplication;
import org.springframework.kafka.annotation.KafkaListener;
import org. spring framework. stereotype.Service;

@SpringBootApplication
public class KafkaConsumerApplication {

    public static void main(String[] args) {
        SpringApplication.run(KafkaConsumerApplication.class, args);
 }

 @Service
    class ConsumerService {
 @KafkaListener(topics = "test topic", groupId = "test group")
        public void listen(String message) {
            System. out.println("Received message: " + message);
 }
 }
}
Enter fullscreen mode Exit fullscreen mode
  1. Run the consumer application:

. In the VS Code terminal, run ./mvnw spring-boot: run.

  1. Test the services:

. Open a web browser and navigate to http://localhost:8080/send?message=Hello, Amazon! The consumer should log "Received message: Hello, Amazon!".

Lessons Learned

. Autonomous Teams: Small, autonomous teams can develop and deploy features faster.

. Service Ownership: Clear ownership improves quality and reliability.

. Event-Driven Communication: Maintains loose coupling and enhances flexibility.

Case Study 3: Uber

  • Overview

Uber transitioned to a microservices architecture to better handle its global operations and improve service reliability.

  • Challenges . Operational Complexity: Managing a monolithic application across multiple regions was difficult.

. Scalability: The monolithic architecture couldn't efficiently scale to meet peak demand.

. Development Bottlenecks: Frequent code conflicts and longer release cycles due to a large, intertwined codebase.

  • Strategies

. Domain-Driven Design: Implemented domain-driven design to break down the monolith into domain-specific services.

. API Gateway: Used an API gateway to manage and route requests between microservices.

. Resilience Engineering: Focused on resilience engineering to ensure services could handle failures gracefully.

  • Coding Example: API Gateway with Spring Cloud Gateway

  • Setting Up the API Gateway

  1. Create a new Spring Boot project:

. Use Spring Initializr to generate a new project.

. Select dependencies: Spring Cloud Gateway, Spring Boot DevTools, and Spring Web.

  1. Open the project in VS Code:

. Unzip the downloaded project and open it in VS Code.

  1. Define the API Gateway:

. Open src/main/java/com/example/gateway/GatewayApplication.java and replace its contents with the following code:

package com.example.gateway;

import org. spring framework. boot.SpringApplication;
import org. spring framework.boot.autoconfigure.SpringBootApplication;
import org. spring framework.cloud.gateway.route.RouteLocator;
import org. spring framework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org. spring framework.context.annotation.Bean;

@SpringBootApplication
public class GatewayApplication {

    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
 }

 @Bean
    public RouteLocator routeLocator(RouteLocatorBuilder builder) {
        return builder.routes()
 .route("hello_route", r -> r.path("/hello")
 .uri("http://localhost:8081"))
 .build();
 }
}
Enter fullscreen mode Exit fullscreen mode

Setting Up a Backend Service

  1. Create a new Spring Boot project:

. Use Spring Initializr to generate a new project.
. Select dependencies: Spring Web, Spring Boot DevTools.

  1. Open the project in VS Code:

. Unzip the downloaded project and open it in VS Code.

  1. Define the backend service:

. Open src/main/java/com/example/backend/BackendApplication.java and replace its contents with the following code:

Copy code
package com. example.backend;

import org. spring framework. boot.SpringApplication;
import org. spring framework.boot.autoconfigure.SpringBootApplication;
import org. spring framework.web.bind.annotation.GetMapping;
import org. spring framework.web.bind.annotation.RestController;

@SpringBootApplication
public class BackendApplication {

    public static void main(String[] args) {
        SpringApplication.run(BackendApplication.class, args);
 }

 @RestController
    class HelloController {
 @GetMapping("/hello")
        public String sayHello() {
            return "Hello, Uber!";
 }
 }
}
Enter fullscreen mode Exit fullscreen mode
  1. Run the backend service:

. In the VS Code terminal, run ./mvnw spring-boot: run.

  1. Run the API Gateway:

. In the VS Code terminal, navigate to the gateway project directory and run ./mvnw spring-boot: run.

  1. Test the API Gateway:

. Open a web browser and navigate to http://localhost:8080/hello. You should see "Hello, Uber!" served through the API Gateway.

Lessons Learned

. Domain-Driven Design: Helps in breaking down the application into manageable and cohesive services.

. API Gateway: Simplifies routing and helps in managing cross-cutting concerns such as authentication and rate limiting.

. Resilience Engineering: Ensures that services can handle failures gracefully, improving overall reliability.

Conclusion

Transitioning from a monolithic architecture to microservices is a complex but rewarding journey. Companies like Netflix, Amazon, and Uber have demonstrated the benefits of microservices in terms of scalability, flexibility, and resilience. By studying their challenges, strategies, and lessons learned, organizations can better navigate their own transitions. The coding examples provided offer a practical starting point for implementing microservices using Spring Boot, Kafka, and Spring Cloud Gateway. With careful planning, incremental migration, and a focus on automation and resilience, the shift to microservices can lead to significant improvements in application performance and agility.

Top comments (0)