WEBSITE ĐANG PHÁT TRIỂN

Clean code trong deadline gấp - thỏa hiệp thế nào cho đúng

Nói "không bao giờ compromise clean code" là dễ. Thực tế khó hơn nhiều. Bài này là framework tôi dùng sau 20 năm để quyết định: thỏa hiệp cái gì, không thỏa hiệp cái gì - và làm thế nào để không bị debt giết sau này.

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:

  1. Fix đúng cách: refactor order service, viết unit tests, deploy clean - mất 2 ngày
  2. 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
  3. 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


Bài viết liên quan

Xem thêm
Clean Code, Testing & Engineering Excellence

Vibe coding vui đấy, nhưng production thì không đùa được

Vibe coding với AI cực kỳ năng suất - nhưng 53% code AI sinh ra không qua nổi security review. Bài này tôi phân tích những lỗ hổng phổ biến nhất trong AI-generated code và checklist thực tế để vừa vibe vừa không mất ngủ vì production. Anh bạn tôi - một solo founder đang build SaaS app với Cursor - gọi điện lúc 11 giờ đêm tuần trước. "Ông ơi, tôi vừa nhận email từ khách hàng. Họ nói thấy data của người khác trong account của họ." Anh ấy đã dùng Cursor để build toàn bộ cái app trong 3 tuần. AI viết hết - backend, frontend, database schema, authentication. Năng suất khủng khiếp. Anh ấy tự hào lắm. Ra production mới phát hiện: AI đã generate thiếu Row Level Security (RLS) cho Supabase. Mọi user đều đọc được data của nhau. Đây không phải câu chuyện cá biệt. Vibe coding - cái trend "để AI viết hết, mình chỉ describe" - đang thay đổi hoàn toàn cách developer làm việc. Cursor, GitHub Copilot, Claude, v0.dev... Tôi dùng hàng ngày. Năng suất tăng 3-5x là thật. Thời gian từ idea đến MVP giảm từ tuần xuống ngày là thật. Nhưng có một thứ AI rất kém: security sense.

Clean Code, Testing & Engineering Excellence

TDD trong dự án thực - tôi đã thử và đây là kết quả

TDD (Test-Driven Development) thường được khen ngợi như silver bullet. Tôi áp dụng thực sự trong 6 tháng trên một production project và có kết quả... mixed. Đây là honest assessment từ người đã làm.