223 lines
7.1 KiB
Markdown
223 lines
7.1 KiB
Markdown
---
|
|
type: "blog-post"
|
|
title: "Dismantling a Monolithic Golang Application"
|
|
description: "The follow-up article showcases a practical example of how to apply the strangler pattern to a monolithic system, using a Go application as an example. The process involves incrementally extracting small pieces of functionality into separate APIs, testing them, and gradually replacing the old monolithic code. This approach allows for a smoother and less risky transition to a microservices architecture, with minimal disruption to the existing system."
|
|
draft: false
|
|
date: "2023-04-13"
|
|
updates:
|
|
- time: "2023-04-13"
|
|
description: "first iteration"
|
|
tags:
|
|
- '#blog'
|
|
- '#microservices'
|
|
- '#monolith'
|
|
- '#softwarearchitecture'
|
|
- '#golang'
|
|
- '#stranglerpattern'
|
|
- '#migration'
|
|
- '#api'
|
|
- '#deployment'
|
|
---
|
|
|
|
In this follow-up article to
|
|
[Strategies for Dismantling Monolithic Systems](https://blog.kasperhermansen.com/posts/breaking-down-the-monolith/),
|
|
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:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
// 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:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
// 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.
|
|
|
|
```mermaid
|
|
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.
|
|
|
|
```mermaid
|
|
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.
|
|
|
|
```mermaid
|
|
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.
|