Đằng sau đống code “Spaghetti” trong dự án Fintech
Hồi mới join một dự án Fintech viết bằng Go, mình từng “sốc nhiệt” khi đọc codebase. Dự án lúc đó đã chạy hơn một năm với 5 developer. Code xử lý thanh toán, kết nối DB và gửi thông báo nằm lẫn lộn, đan xen nhau như mạng nhện. Mỗi lần cần thêm một cổng thanh toán mới như Momo hay ZaloPay, anh em lại phải hì hục copy-paste hàng chục dòng if-else lồng nhau.
Hệ lụy thấy rõ ngay lập tức. Việc viết Unit test trở thành cực hình vì các thành phần bị dính chặt (tight coupling). Một lần mình chỉ sửa lỗi hiển thị ở module thông báo, nhưng không hiểu sao lại làm sập luôn luồng thanh toán. Đây là cái giá phải trả khi coi thường Design Patterns và nghĩ rằng Go chỉ cần viết đơn giản là đủ.
Tại sao Go vẫn cần Design Patterns?
Nhiều người cho rằng Go đề cao sự tối giản nên các pattern của Java hay C++ là rườm rà. Thực tế ngược lại. Khi dự án vượt ngưỡng 10.000 dòng code, nếu thiếu bộ khung chuẩn, hệ thống của bạn sẽ sớm trở thành một đống nợ kỹ thuật (technical debt).
Trong quá trình tái cấu trúc dự án đó, mình đã áp dụng 3 pattern then chốt: Singleton, Factory, và Strategy. Kết quả rất ấn tượng. Tốc độ phát triển tính năng mới tăng rõ rệt, và quan trọng là anh em không còn sợ mỗi khi deploy vào chiều thứ Sáu nữa.
1. Singleton Pattern: Quản lý tài nguyên tập trung
Bài toán: Database connection đạt ngưỡng giới hạn
Ở dự án cũ, dev thường khởi tạo connection mới một cách tùy tiện hoặc dùng biến global thiếu kiểm soát. Khi traffic đạt mốc 1.000 request/giây, hệ thống lập tức báo lỗi too many connections do leak tài nguyên.
Giải pháp: Triển khai với sync.Once
Trong Go, đừng dùng init() để làm Singleton. Cách chuẩn nhất là dùng sync.Once. Cơ chế này đảm bảo hàm khởi tạo chỉ chạy duy nhất một lần, bất kể có bao nhiêu goroutine đang tranh chấp.
package database
import (
"sync"
"database/sql"
_ "github.com/lib/pq"
)
type dbInstance struct {
conn *sql.DB
}
var (
instance *dbInstance
once sync.Once
)
func GetInstance() *dbInstance {
once.Do(func() {
// Khởi tạo kết nối Postgres duy nhất
db, _ := sql.Open("postgres", "user=admin dbname=fintech sslmode=disable")
db.SetMaxOpenConns(25) // Giới hạn connection pool
instance = &dbInstance{conn: db}
})
return instance
}
Nhờ cách này, mình kiểm soát được chính xác số lượng connection. Tình trạng tràn bộ nhớ hay treo DB hoàn toàn biến mất.
2. Factory Method: Đóng gói logic khởi tạo
Bài toán: Hệ thống Notification đa kênh
Ban đầu, mỗi khi cần gửi tin nhắn, code của team tràn ngập logic kiểm tra loại thông báo. Cứ thêm một kênh mới như Telegram hay Viber, team lại mất cả buổi sáng để rà soát và sửa if-else ở khắp các file.
Giải pháp: Dùng Interface làm bản thiết kế
Mình định nghĩa một interface chung và một Factory để tách biệt logic tạo object khỏi logic nghiệp vụ.
type Notification interface {
Send(message string)
}
type EmailNotification struct{}
func (e EmailNotification) Send(msg string) { /* Logic API SendGrid */ }
type SMSNotification struct{}
func (s SMSNotification) Send(msg string) { /* Logic API Twilio */ }
func NewNotification(notifyType string) Notification {
switch notifyType {
case "email":
return EmailNotification{}
case "sms":
return SMSNotification{}
default:
return nil
}
}
Giờ đây, logic gửi tin chỉ gói gọn trong một dòng: NewNotification("email").Send("Xác thực OTP"). Code sạch hơn hẳn.
3. Strategy Pattern: Linh hoạt thay đổi thuật toán
Bài toán: Ma trận phí vận chuyển
Mỗi đối tác giao hàng (GHN, Viettel Post, Grab) có một cách tính phí riêng dựa trên khối lượng và khoảng cách. Nếu viết chung vào một hàm, hàm đó sẽ dài hàng trăm dòng và cực kỳ khó hiểu.
Cách triển khai thực tế
Mỗi đơn vị vận chuyển là một “Strategy”. Bạn có thể thay đổi chúng như thay một chiếc áo mà không làm ảnh hưởng đến cơ thể (đối tượng Order).
type ShippingStrategy interface {
Calculate(weight float64) float64
}
type GHNStrategy struct{}
func (g GHNStrategy) Calculate(w float64) float64 { return w * 1.2 }
type GrabStrategy struct{}
func (g GrabStrategy) Calculate(w float64) float64 { return w * 2.5 }
type Order struct {
weight float64
strategy ShippingStrategy
}
func (o *Order) SetStrategy(s ShippingStrategy) {
o.strategy = s
}
func (o *Order) GetShippingCost() float64 {
return o.strategy.Calculate(o.weight)
}
Khi sếp yêu cầu thêm đơn vị vận chuyển mới, mình chỉ mất 5 phút tạo struct mới. Không cần đụng vào code cũ, đảm bảo đúng nguyên tắc Open/Closed.
Đúc kết sau 6 tháng vận hành thực tế
Áp dụng pattern không phải là để “làm màu”. Nó giúp hệ thống sống sót khi scale lớn. Dưới đây là 3 lưu ý nhỏ cho anh em:
- Tránh Over-engineering: Nếu logic đơn giản, đừng cố nhét Factory vào. Chỉ dùng khi bạn thực sự thấy sự lặp lại.
- Interface nhỏ là tốt nhất: Trong Go, hãy giữ interface gọn nhẹ, thường chỉ từ 1 đến 2 method.
- Dễ dàng viết Mock: Nhờ tách biệt interface, việc giả lập (mock) các dịch vụ bên thứ ba khi viết Unit Test trở nên cực kỳ đơn giản.
Nắm vững Design Patterns giúp bạn chuyển từ tư duy “viết cho chạy được” sang “viết để duy trì lâu dài”. Hãy thử áp dụng vào task nhỏ nhất ngay hôm nay, bạn sẽ thấy sự khác biệt.

