Cái đêm tôi viết code xấu nhất đời
Năm 2017, dự án e-commerce flash sale dịp Tết. Launch ngày 23 tháng Chạp. Tôi nhận ra một bug nghiêm trọng trong order pipeline lúc 11 giờ đêm ngày 22.
Tôi có 3 lựa chọn:
- Fix đúng cách: refactor order service, viết unit tests, deploy clean - mất 2 ngày
- Patch nhanh: thêm một if-else xấu xí vào giữa method 200 dòng, viết integration test cơ bản - mất 3 tiếng
- Hoãn launch
Option 3 là đau nhất về business. Option 1 là không khả thi về thời gian. Tôi chọn option 2.
Code đó xấu. Tôi biết nó xấu ngay lúc viết. Nhưng flash sale chạy ổn, không mất đơn hàng nào. Và 2 tuần sau Tết, tôi refactor đúng cách.
Đó là ví dụ về technical debt có ý thức - tôi biết mình đang vay, tôi biết khi nào trả, và tôi đã trả.
Quay lại chuyện kỹ thuật: Debt có chủ đích vs Debt vô tình
Martin Fowler có một technical debt quadrant rất hay. Tôi simplify nó thành 2 loại:
Intentional debt (debt có chủ đích):
- Bạn biết code này không clean
- Bạn quyết định accept nó với lý do rõ ràng
- Bạn có plan để fix sau (và thực sự fix)
Accidental debt (debt vô tình):
- Code xấu vì bạn không biết tốt hơn, hoặc
- Code xấu vì rushed mà không ai chú ý, hoặc
- Code xấu accumulate qua nhiều năm không ai maintain
Loại thứ nhất - manageable. Loại thứ hai - nguy hiểm.
Vấn đề là: nhiều team gọi accidental debt là intentional để tự biện hộ. "Chúng ta biết code này không tốt, nhưng không có thời gian" - nhưng không có plan để fix - đó là self-deception.
Framework quyết định: 4 câu hỏi
Khi đứng trước deadline và cần quyết định thỏa hiệp hay không, tôi hỏi 4 câu:
Câu 1: "Có ai khác sẽ phải đọc/sửa code này không?"
Nếu code này chỉ tôi dùng, internal tool ít người dùng, hoặc prototype - threshold để thỏa hiệp thấp hơn.
Nếu code này là core business logic, nhiều người maintain, hoặc customer-facing - threshold cao hơn nhiều.
Câu 2: "Nếu tôi để code này như vậy 6 tháng, hệ quả là gì?"
Một variable đặt tên tệ: acceptable - context mất đi nhưng không gây bug.
Một method 300 dòng làm 5 việc khác nhau: dangerous - mỗi lần có người cần sửa sẽ mất 2 tiếng để hiểu.
Logic validate business rule rải rác 10 chỗ: very dangerous - inconsistency bugs đang chờ.
Câu 3: "Khi nào tôi sẽ fix điều này?"
Nếu không có câu trả lời cụ thể - đừng tạo debt.
Tôi dùng "debt ledger" - một file markdown đơn giản:
# Technical Debt Ledger
## 2026-01-17
- **OrderService.ProcessOrder()**: Temporary if-else để handle edge case Tết
- **Lý do accept:** Flash sale launch 23/1, không đủ thời gian refactor đúng
- **Hạn fix:** 2026-02-10 (sau Tết)
- **Owner:** Son Do
- **Estimate:** 4 giờ
Nếu bạn không sẵn sàng viết vào debt ledger - bạn chưa sẵn sàng tạo debt.
Câu 4: "Thỏa hiệp ở đây cụ thể là gì?"
"Code xấu hơn" quá mơ hồ. Hãy cụ thể:
✅ Acceptable shortcuts:
- Skip viết unit test cho thin wrapper (integration test cover)
- Dùng hardcoded value tạm thời thay vì config
- Tên variable ngắn trong method ngắn (<20 dòng)
- Comment TODO thay vì refactor ngay
❌ Không thể thỏa hiệp:
- Logic business rule không có test gì cả
- Copy-paste code có điều kiện (duplication + drift risk)
- Error handling bị bỏ qua hoàn toàn
- Security validation bị skip
Real-world application
// TRƯỚC KHI THỎA HIỆP - code lý tưởng
public class OrderProcessor
{
private readonly IInventoryService _inventoryService;
private readonly IPaymentGateway _paymentGateway;
private readonly INotificationService _notificationService;
private readonly IOrderRepository _orderRepository;
public async Task<OrderResult> ProcessOrderAsync(CreateOrderCommand command)
{
await ValidateInventoryAsync(command);
var payment = await ProcessPaymentAsync(command);
var order = await CreateOrderAsync(command, payment);
await NotifyCustomerAsync(order);
return OrderResult.Success(order);
}
// ...clean separation of concerns...
}
// SAU KHI THỎA HIỆP - acceptable shortcut cho deadline
public class OrderProcessor
{
// TODO: [DEBT-2026-01-17] Tách NotifyCustomer thành async background job
// Current: sync notification làm chậm response ~200ms
// Fix by: 2026-02-10
public async Task<OrderResult> ProcessOrderAsync(CreateOrderCommand command)
{
await ValidateInventoryAsync(command);
var payment = await ProcessPaymentAsync(command);
var order = await CreateOrderAsync(command, payment);
// Temporary: sync call thay vì queue - acceptable vì flash sale ngắn hạn
await _notificationService.SendConfirmationAsync(order);
return OrderResult.Success(order);
}
}
Comment TODO không phải lười biếng - đó là intentional debt với context đầy đủ.
Kinh nghiệm từ dự án thực
Trong team tôi, có quy tắc: Mỗi sprint review, chúng tôi dành 15 phút xem debt ledger và prioritize.
Không phải mọi debt đều được fix ngay. Nhưng không có debt nào "vô hình" - tất cả đều được nhìn thấy và quyết định có ý thức.
Kết quả sau 2 năm áp dụng: số lần production incident do technical debt giảm đáng kể. Team không còn sợ deadline vì biết rằng shortcuts có process, không phải là "chấp nhận tệ đi mãi mãi".
Triết lý
Clean code không phải là tôn giáo. Nó là công cụ để build software tốt hơn trong dài hạn.
Đôi khi "clean code 100%" hôm nay có nghĩa là miss deadline và business opportunity. Đó không phải trade-off tốt.
Nhưng "thoải mái với code xấu vì deadline" mà không có plan fix là con đường dẫn đến hệ thống không ai dám sửa, không ai hiểu, và cuối cùng phải đập đi xây lại.
Balanced approach: Biết cái gì có thể thỏa hiệp tạm thời. Biết cái gì không bao giờ được thỏa hiệp. Và luôn có plan trả debt.
Bạn đã gặp tình huống này chưa?
Bạn đã từng phải chọn giữa clean code và deadline? Bạn đã handle thế nào? Và quan trọng hơn - cái "shortcut" đó sau này đã được trả chưa? Kể cho tôi nghe :)
/Son Do - believe in basic
#1percentbetter #CleanCode #TechnicalDebt #SoftwareEngineering #dotnet