This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
---
|
||||
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.
|
Reference in New Issue
Block a user