Tại Sao Phải Chia Layer Trước Khi Viết Bất Kỳ Dòng Code Nào?
Trong thế giới phần mềm, đặc biệt là backend API, có một bài học mà hầu như đội kỹ thuật nào cũng từng rút ra, đôi khi bằng cái giá rất đắt:
Nếu bạn không thiết kế kiến trúc ngay từ đầu, sau này bạn sẽ phải trả giá gấp mười lần để sửa lại.
Dự án nhỏ ba endpoint có thể chạy tốt dù code hơi bừa bộn.
Nhưng khi bạn thêm user service, auth, billing, cron job, caching, mailing..., mọi thứ bắt đầu nối vào nhau như một mớ mì spaghetti. Chỉ cần đổi một câu query hay thêm một API, hệ thống có thể đổ vỡ như hiệu ứng domino.
Vấn đề không phải nằm ở công nghệ bạn dùng, mà nằm ở cách bạn tổ chức code.
Và cách tổ chức đúng đắn nhất, được cộng đồng backend đồng thuận nhiều năm nay, là chia hệ thống thành các layer (tầng).
1. Hành Trình Của Một HTTP Request
Để hiểu vì sao cần chia layer, hãy nhìn hành trình của một request đơn giản:
Người dùng muốn gọi API để lấy danh sách bài viết.
Luồng xử lý thực tế:
User → Browser / App → Gửi HTTP Request
↓
Router định tuyến request đến đúng handler
↓
Handler đọc input, validate dữ liệu
↓
Service xử lý business logic
↓
Repository/Storage truy cập database hoặc cache
↓
Database trả dữ liệu
↓
Service tổng hợp kết quả
↓
Handler trả response JSON về cho user
Nếu bạn không chia từng phần này thành các tầng độc lập, handler rất dễ:
- Nhét logic vào trực tiếp
- Gọi SQL query thẳng trong controller
- Xử lý dữ liệu, validate, format response trong cùng một file
Chỉ sau vài tuần, bạn sẽ không còn phân biệt nổi đâu là business logic, đâu là xử lý HTTP, đâu là truy cập database.
Kiến trúc chính là điều giúp hành trình request trở nên rành mạch.
2. Những Nguyên Tắc Kiến Trúc Quan Trọng
Những nguyên tắc dưới đây dựa trên tư tưởng Clean Architecture của Robert C. Martin, nhưng được đơn giản hóa để bạn áp dụng ngay mà không cần đọc cả quyển sách hơn 400 trang.
2.1 Separation of Concerns
Tách biệt trách nhiệm là nền tảng của kiến trúc sạch.
Ở cấp API backend, có thể hiểu đơn giản như sau:
| Layer | Trách nhiệm |
|---|---|
| Transport (Router/Handler) | Nhận request, validate, trả response |
| Service (Business Logic) | Xử lý nghiệp vụ, dùng các trường hợp sử dụng |
| Repository (Data Access) | Giao tiếp database, cache, external service |
| Database | Lưu trữ dữ liệu |
Không layer nào được làm công việc của layer khác.
Ví dụ: Một thiết kế sai phổ biến
func CreateOrder(w http.ResponseWriter, r *http.Request) {
var req CreateOrderRequest
json.NewDecoder(r.Body).Decode(&req)
// Validate request
if req.Amount <= 0 {
http.Error(w, "invalid amount", 400)
return
}
// Business logic
if req.Amount > 1000 {
req.Discount = 0.1
}
// Database logic
_, err := db.Exec("INSERT INTO orders (amount, discount) VALUES (?, ?)", req.Amount, req.Discount)
if err != nil {
http.Error(w, "db error", 500)
return
}
json.NewEncoder(w).Encode("ok")
}
Một hàm xử lý cả HTTP, business logic lẫn database.
Ngay khi add thêm vài feature, file này sẽ trở thành “vùng cấm”.
Ví dụ: Cách làm đúng
Ta có:
Handler → gọi → Service → gọi → Repository → gọi → Database
Triển khai:
// Handler
func (h *OrderHandler) Create(w http.ResponseWriter, r *http.Request) {
var input CreateOrderInput
json.NewDecoder(r.Body).Decode(&input)
output, err := h.service.CreateOrder(input)
if err != nil {
http.Error(w, err.Error(), 400)
return
}
json.NewEncoder(w).Encode(output)
}
// Service
func (s *OrderService) CreateOrder(input CreateOrderInput) (Order, error) {
if input.Amount <= 0 {
return Order{}, errors.New("invalid amount")
}
discount := 0.0
if input.Amount > 1000 {
discount = 0.1
}
order := Order{Amount: input.Amount, Discount: discount}
return s.repo.Save(order)
}
// Repository
func (r *OrderRepository) Save(o Order) (Order, error) {
_, err := r.db.Exec("INSERT INTO orders (amount, discount) VALUES (?, ?)", o.Amount, o.Discount)
return o, err
}
Kiến trúc gọn gàng tạo ra code dễ đọc, dễ test, dễ mở rộng.
2.2 Dependency Inversion Principle (DIP)
Đây là nguyên tắc được ứng dụng rất nhiều trong backend thực chiến.
Ý tưởng:
Service không phụ thuộc trực tiếp vào database thật, mà phụ thuộc vào interface của repository.
Ví dụ:
type OrderRepository interface {
Save(Order) (Order, error)
FindByID(id int) (Order, error)
}
Nhờ vậy bạn có thể thay đổi cách lưu trữ mà không chạm vào business layer:
- PostgreSQL → MySQL
- Redis → in-memory
- S3 → local file
Repository thật → mock repository cho unit test
Service chỉ cần interface:
type OrderService struct {
repo OrderRepository
}
Bạn có thể truyền bất kỳ implementation nào vào service.
Đây chính là yếu tố giúp ứng dụng sống lâu, thay đổi dễ dàng.
2.3 Cascading Layers và mô hình dạng Onion
Hãy tưởng tượng hệ thống như củ hành:

[Transport Layer]
[Service Layer]
[Repository Layer]
[Database / External System]
Luồng chảy:
Transport → Service → Repository → Database
Điều cấm kỵ trong Clean Architecture:
- Repository không bao giờ gọi Service
- Service không gọi Handler
- Database không biết gì về application logic
- Handler không được chứa business logic
Giữ cho phụ thuộc một chiều giúp code không bị vòng lặp và dễ dàng thay đổi từng phần.
2.4 Kiến trúc sạch giúp việc test trở nên đơn giản
Một service khi được tách đúng sẽ dễ dàng test mà không cần database thật.
Ví dụ mock repository:
type MockRepo struct {
saved Order
}
func (m *MockRepo) Save(o Order) (Order, error) {
m.saved = o
return o, nil
}
Khi Test:
func TestOrderService_CreateOrder(t *testing.T) {
repo := &MockRepo{}
service := OrderService{repo}
order, err := service.CreateOrder(CreateOrderInput{Amount: 2000})
if err != nil {
t.Fatal(err)
}
if order.Discount != 0.1 {
t.Errorf("expected 10%% discount, got %.2f", order.Discount)
}
}
Không database, không network, không side effects.
2.5 Dễ bảo trì và mở rộng
Một hệ thống có cấu trúc tốt sẽ mang lại nhiều lợi ích:
- Thêm tính năng mới mà không ảnh hưởng code cũ
- Đổi công nghệ database mà không phá vỡ logic
- Tái cấu trúc (refactor) nhanh và an toàn
- Giảm xung đột khi nhiều lập trình viên làm việc chung
- Đọc code của người khác dễ dàng
Khi dự án vượt quá hàng nghìn dòng code, cấu trúc chính là thứ giữ cho kiến trúc không sụp đổ.
2.6 Tập trung vào giá trị thực thay vì sửa lỗi kiến trúc
Khi kiến trúc không rõ ràng, developer phải chi phần lớn thời gian:
- lần theo bug vòng vèo qua nhiều file
- sửa lỗi phát sinh từ việc thay đổi logic cũ
- đập đi làm lại vì khó mở rộng
Ngược lại, khi kiến trúc sạch:
- bạn thêm tính năng mới nhanh hơn
- bạn ít chỉnh sửa code cũ
- bạn dành thời gian cải thiện giá trị cho người dùng
- dự án mở rộng tự nhiên, không gây áp lực cho kỹ thuật
Kiến trúc sạch không chỉ giúp code đẹp, mà còn giúp đội ngũ chạy nhanh hơn và ổn định hơn.
3. Ví dụ: Folder structure đơn giản theo layered architecture
Một dự án Go có thể tổ chức như sau:
social/
│
├── bin/
├── cmd/
│ ├── api/
│ └── migrate/
│ └── migrations/
│
├── api/
│ ├── handlers/
│ ├── middleware/
│ └── router.go
│
├── internal/
│ ├── storage/
│ ├── email/
│ ├── ratelimiter/
│ ├── services/
│ └── mocks/
│
├── docs/
├── scripts/
└── web/ (optional)
Mỗi phần có vai trò rõ ràng, không ai bước qua ranh giới của ai.
4. Kết luận
Chia layer không phải là xu hướng nhất thời. Nó là bài học được rút ra từ hàng chục năm phát triển phần mềm.
Khi bạn chia layer rõ ràng:
- code dễ đọc
- logic rõ ràng
- test đơn giản
- sửa nhanh, mở rộng dễ
- giảm rủi ro khi dự án lớn
Một hệ thống không có kiến trúc chuẩn ngay từ đầu sẽ phải trả giá gấp nhiều lần về sau.
All rights reserved