Dismantling a Monolithic Golang Application

@kjuulh

2023-04-13

In this follow-up article to Strategies for Dismantling Monolithic Systems, we will explore a practical example of dismantling a monolithic Golang application using the strategies discussed in the previous article. We will walk through the process step by step, demonstrating the application of the Strangler, Decorator, and Sprig strategies, and provide a simple diagram to illustrate the architectural changes.

Initial Monolithic Application

Consider a simple monolithic Golang application that handles user registration and authentication:

package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/register", registerHandler)
	http.HandleFunc("/login", loginHandler)
	http.ListenAndServe(":8080", nil)
}

func registerHandler(w http.ResponseWriter, r *http.Request) {
	// Register user logic
	fmt.Fprint(w, "User registered")
}

func loginHandler(w http.ResponseWriter, r *http.Request) {
	// Authenticate user logic
	fmt.Fprint(w, "User logged in")
}

This application has two main functionalities: registering a new user and logging in an existing user.

Breaking Down the Monolith

Step 1: Identify the functionalities to be extracted

We will start by identifying the functionalities that can be extracted into separate microservices. In our example, we will extract the user registration and authentication functionalities into two separate services:

  1. User Registration Service
  2. Authentication Service

Step 2: Apply the Strangler Pattern

Next, we will apply the Strangler Pattern to gradually replace the monolithic application with the new microservices.

First, create the new User Registration and Authentication services:

// User Registration Service
func newUserRegistrationHandler(w http.ResponseWriter, r *http.Request) {
	// New user registration logic
	fmt.Fprint(w, "New user registered")
}

// Authentication Service
func newAuthenticationHandler(w http.ResponseWriter, r *http.Request) {
	// New authentication logic
	fmt.Fprint(w, "New user authenticated")
}

Now, we will modify the main function of the application to use these new services:

func main() {
	http.HandleFunc("/register", newUserRegistrationHandler)
	http.HandleFunc("/login", newAuthenticationHandler)
	http.ListenAndServe(":8080", nil)
}

During this transition, we can use feature flags or canary deployments to control the traffic between the old and new services.

Step 3: Apply the Decorator and Sprig Patterns

As we develop new features, we can leverage the Decorator and Sprig Patterns to add functionality to the new microservices without further complicating the monolithic application.

For example, if we want to implement a password reset functionality, we can create a new endpoint in the Authentication Service:

// Password Reset
func passwordResetHandler(w http.ResponseWriter, r *http.Request) {
	// Password reset logic
	fmt.Fprint(w, "Password reset")
}

// Updated main function
func main() {
	http.HandleFunc("/register", newUserRegistrationHandler)
	http.HandleFunc("/login", newAuthenticationHandler)
	http.HandleFunc("/reset-password", passwordResetHandler)
	http.ListenAndServe(":8080", nil)
}

By following these strategies, we can gradually dismantle the monolithic application while maintaining a functional system throughout the process.

System Diagram

Step 1: Identify and Isolate a Component

The first step is to identify and isolate a component that can be extracted from the monolith. This should be a well-defined, self-contained unit that can be broken off and turned into a separate service without affecting the rest of the application. Once you have identified the component, you should create a new API that can handle its responsibilities.

graph TD
    A[User] --> B[Monolith]
    B --> C[Monolithic Application]
    subgraph C["Monolithic Application"]
        E[UserRegistrationService]
        F[AuthenticationService]
    end

Step 2: Create a New API

Next, you need to create a new API that can handle the responsibilities of the isolated component. This API should be designed to work independently of the monolith, so it can be easily swapped in or out as needed. The API should be thoroughly tested to ensure it works as expected.

Step 3: Test and Roll Out the New API

Once you have created the new API, you need to test it to ensure it works as expected. You can use a canary rollout or feature flags to gradually roll out the new API while still keeping the old one in place. This will allow you to catch any issues or bugs before fully switching over to the new API.

Step 4: Switch Over to the New API

Once you have thoroughly tested the new API, it's time to switch over to it. You can do this by updating the monolith to use the new API instead of the old one. You should monitor the application closely to ensure there are no issues or bugs, and be prepared to roll back if necessary.

graph TD
    A[User] --> B[Monolith]
    B --> C[Monolithic Application]
    B --> D
    subgraph C[Monolithic Application]
        E[UserRegistrationService]
        F[AuthenticationService]
    end
    subgraph D[Microservices]
        NewUserRegistrationService
        NewAuthenticationService
    end

Step 5: Remove monolithic application

Once you're satisfied with the performance of the new API, delete the old parts of the monolith. This process can take a long time, as the old code will exist as a form of backup for a while.

graph TD
    A[User] --> B[Microservices]
    subgraph B["Microservices"]
        C[UserRegistrationService]
        D[AuthenticationService]
        E[PasswordResetService]
    end

Step 6: Repeat

Finally, you should repeat the process by identifying and isolating another component that can be extracted from the monolith. This process can be repeated until the monolith has been completely broken down into a set of smaller, independent services.

Conclusion

By following these strategies, we can gradually dismantle a monolithic Golang application while maintaining a functional system throughout the process. The practical example and the PlantUML diagram demonstrate how the Strangler, Decorator, and Sprig Patterns can be applied to effectively break down a monolithic application into smaller, more manageable microservices.