16.2 — 1. Vì sao CRM “to ra” là lúc kiến trúc trả giá?
Kiến trúc không phải để code "đẹp" — nó để chi phí thay đổi không tăng theo kích thước dự án. Trong một codebase khoẻ, thêm tính năng thứ 100 tốn xấp xỉ bằng thêm tính năng thứ 10. Trong một codebase mục, tính năng thứ 100 tốn gấp mười. Ba dấu hiệu đo được cho biết bạn đang ở đâu: một thay đổi nghiệp vụ nhỏ phải sửa nhiều file ở nhiều tầng, không viết được unit test mà không dựng database, và sửa chỗ A làm hỏng chỗ B không liên quan. Nhưng có một sai lầm ngược lại cũng đắt: áp kiến trúc phức tạp quá sớm. Với một CRUD 10 bảng, bốn project và MediatR là chi phí thuần — và bạn chưa biết ranh giới nghiệp vụ nằm ở đâu để đặt cho đúng.
Mục tiêu bài học
Sau bài này bạn có thể:
- Nhận ra ba dấu hiệu đo được của code đang mục.
- Giải thích vì sao chi phí thay đổi tăng.
- Tránh cả hai sai lầm: quá muộn và quá sớm.
- Chọn thời điểm tách lớp có cơ sở.
Nội dung bài học
16.2.1 — Chi phí thay đổi là thước đo duy nhất
Mọi lập luận kiến trúc cuối cùng quy về một câu hỏi: thay đổi nghiệp vụ tiếp theo tốn bao nhiêu?
Codebase khoe: tinh nang thu 100 ~ tinh nang thu 10
Codebase mục: tính năng thứ 100 = 10x tính năng thứ 10
Điều làm chi phí tăng không phải số dòng code, mà là số thứ bạn phải hiểu để sửa an toàn. Khi một quy tắc nghiệp vụ nằm rải ở controller, service, trigger database và một job nền, bạn phải biết cả bốn trước khi đổi nó.
Ba dấu hiệu đo được:
1. Một thay đổi nghiệp vụ phải sửa nhiều tầng.
"Khách hàng hạng Bronze không được tạo deal trên 500 triệu" — đếm số file phải sửa. Nếu là một, kiến trúc đang làm việc của nó. Nếu là sáu (controller, service, validator, trigger, báo cáo, job), quy tắc đó không có nhà.
2. Không viết được unit test mà không dựng database.
Nếu để test một quy tắc nghiệp vụ bạn phải dựng DbContext, seed dữ liệu và dọn dẹp, thì quy tắc đó không tách khỏi hạ tầng. Đó là dấu hiệu rõ nhất và dễ kiểm tra nhất.
3. Sửa chỗ A làm hỏng chỗ B không liên quan.
Sửa tính năng Billing làm vỡ màn hình Lead nghĩa là chúng ghép chặt qua một thứ dùng chung — thường là một entity khổng lồ hoặc một service "God" mà mọi thứ đều gọi.
16.2.2 — Nó mục như thế nào
Không ai cố tình viết code xấu. Nó xảy ra theo từng bước hợp lý:
// Thang 1 — hoan toan on
[HttpPost]
public async Task<IActionResult> Create(CreateLeadRequest request, CancellationToken ct)
{
var lead = new Lead { Name = request.Name, Value = request.Value };
_db.Leads.Add(lead);
await _db.SaveChangesAsync(ct);
return Ok(lead);
}
// Tháng 6 — mỗi dòng thêm vào đều "hợp lý"
[HttpPost]
public async Task<IActionResult> Create(CreateLeadRequest request, CancellationToken ct)
{
if (request.Value > 500_000_000 && _currentUser.Tier == "Bronze")
return BadRequest("Vuot han muc"); // quy tac nghiep vu
var duplicate = await _db.Leads.AnyAsync(l => l.Email == request.Email, ct);
if (duplicate) return Conflict(); // quy tac nghiep vu
var lead = new Lead { ... };
lead.Score = request.Value > 100_000_000 ? 10 : 5; // quy tac nghiep vu
_db.Leads.Add(lead);
await _db.SaveChangesAsync(ct);
await _email.SendAsync(...); // tac dung phu
await _crmSync.PushAsync(lead, ct); // tac dung phu
await _audit.LogAsync(...); // tac dung phu
return Ok(lead);
}
Không có dòng nào sai. Nhưng bây giờ:
- Quy tắc "Bronze không được vượt 500 triệu" chỉ tồn tại ở đây — import hàng loạt bỏ qua nó hoàn toàn.
- Test quy tắc đó cần
HttpContext,DbContext, và mock ba service. - Cùng quy tắc sẽ được chép lại vào job import và vào màn hình admin — rồi ba bản lệch nhau.
Đó là cách một codebase mục: không phải một quyết định tồi, mà 200 quyết định hợp lý không có nơi nào để đặt logic nghiệp vụ.
16.2.3 — Sai lầm ngược: kiến trúc quá sớm
CRM ban đầu: 10 bảng, 20 endpoint, CRUD thuần
Kiến trúc áp dụng: 4 project, MediatR, repository, specification, domain event
Với dự án đó, bạn vừa thêm:
| Chi phí | Chi tiết |
|---|---|
| Ba file cho một endpoint | Command, Handler, Validator — thay vì một action |
| Khó lần theo luồng | "Ai xử lý command này?" cần tìm kiếm toàn giải pháp |
| Người mới mất một tuần | Chỉ để hiểu cấu trúc trước khi viết dòng đầu tiên |
| Ranh giới đặt sai | Vì bạn chưa biết nghiệp vụ đủ rõ |
Hàng cuối là chi phí lớn nhất và ít ai nói tới. Kiến trúc là việc vẽ ranh giới, và ranh giới đúng đến từ hiểu nghiệp vụ. Tháng đầu tiên bạn chưa hiểu, nên ranh giới bạn vẽ gần như chắc chắn sai — và sửa ranh giới sai đắt hơn không có ranh giới.
Quy tắc thực dụng:
| Quy mô | Nên |
|---|---|
| CRUD thuần, ít quy tắc | Một project, controller gọi service |
| Bắt đầu có quy tắc nghiệp vụ | Tách Domain khỏi hạ tầng |
| Nhiều use case, nhiều tác dụng phụ | Thêm tầng Application |
| Nhiều đội làm song song | Ranh giới module rõ ràng |
Đi theo thứ tự, và chuyển bước khi có dấu hiệu, không theo lịch.