Dismantling a Monolithic Golang Application
@kjuulh2023-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:
- User Registration Service
- 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.