+2

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:

diagram-export-12-9-2025-11_35_58-AM.png

[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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí