Technical debt không phải “code xấu” — nó là chi phí cơ hội + rủi ro vì quyết định trong quá khứ (có chủ ý hay không). Middle+ cần khung để định giátrả nợ có kế hoạch, thay vì than phiền trong retro.

Phạm vi: sản phẩm phần mềm dịch vụ. Không: accounting tài chính chi tiết.


1. Phân loại nợ

1.1. Deliberate vs accidental

  • Deliberate: “ship MVP trước, chấp nhận schema tạm” — có ghi nhận, có owner.
  • Accidental: thiếu kiến thức, thiếu thời gian test — dễ lan.

1.2. Local vs systemic

  • Local: một module khó đọc nhưng biên giới rõ — refactor incremental an toàn.
  • Systemic: mọi team phụ thuộc vào cùng một “god service” / schema không version — đắt để sửa.

2. “Lãi suất” nợ: làm sao biết nợ đang “ăn lãi”

Tín hiệu thực dụng:

  • Thời gian lead time feature cùng loại tăng theo quý.
  • Incident hoặc bug P1/P2 liên quan cùng vùng code/schema.
  • Onboarding engineer mới: thời gian “đụng production an toàn” kéo dài.

Nếu không đo được gì, PM nghe như cảm xúc.


3. Chiến lược trả nợ

3.1. Strangler fig

Thay thế dần bề mặt: route traffic/migrate API theo slice; giữ hệ cũ chạy song song cho đến khi cắt.

3.2. Freeze surface

Ngừng mở rộng API/schema legacy; mọi feature mới đi đường mới — giảm phình nợ.

3.3. Incremental refactor có cờ

Feature flag + test regression + metric — tránh “tắt đèn refactor 3 tháng”.


4. Nói chuyện với PM bằng risk/cost (mẫu câu)

Thay vì: “Code bẩn quá phải refactor.”

Thử: “Vùng billing chiếm 30% incident Q2; nếu không tách module và thêm test matrix, ước lượng mỗi feature billing thêm ~N ngày và rủi ro rollback khó vì coupling X.”

Kèm một lựa chọn rẻ hơn (freeze surface + test tối thiểu) nếu không đủ budget strangler.


5. Khi rewrite là trap

Dấu hiệu:

  • Không ai viết được invariant hệ thống cũ đang đảm bảo.
  • Không có harness so sánh output / shadow traffic.
  • Timeline “6 tháng” không có milestone đo được mỗi 2 tuần.

Rewrite đôi khi đúng — nhưng middle+ phải chứng minh vì sao strangler không đủ.


6. “Interest” của nợ: cách ước lượng định tính

Bạn không cần spreadsheet hoàn hảo. Một cách ước lượng:

  • Tần suất chạm: bao nhiêu PR/quý đụng module này?
  • Mức độ sợ hãi: engineer có tránh refactor vì sợ break không?
  • Chi phí incident: có P1/P2 liên quan không?

Nếu cả ba đều cao → lãi suất cao, ưu tiên trả hoặc freeze surface.


7. Debt trong API contract vs nội bộ

  • Public API / SDK: nợ đắt vì breaking change ảnh hưởng khách hàng — versioning, deprecation window.
  • Nội bộ monolith: có thể refactor mạnh hơn nếu test tốt.

Đừng áp cùng một chiến lược cho cả hai.


8. Boy Scout rule có giới hạn

“Để lại sạch hơn một chút” tốt cho local debt, nhưng với systemic debt, thay đổi nhỏ rải rác không có chủ đề dễ tạo regression. Khi đó cần initiative có owner và milestone.


9. Ví dụ “nói với PM” có số (giả định)

Module X có 40% PR chạm vào trong Q2; median thời gian review PR đụng X là 2.5 ngày vs 0.8 ngày toàn repo. Ước lượng mỗi feature mới tại X thêm ~3 ngày do conflict test + cherry-pick hotfix. Đề xuất: 10 ngày freeze surface + tách adapter + 20 ngày strangler — giảm lead time PR tại X xuống mục tiêu 1.2 ngày trong Q3.

Con số có thể sai, nhưng có thể tranh luận; cảm xúc thì không.


10. Deprecation là một dạng trả nợ

Ngừng hỗ trợ endpoint cũ, xoá field legacy, giảm số version SDK — tất cả là giảm entropy. Lên lịch deprecation công khai (changelog, email API consumers) giúp nợ không tái sinh.


11. Liên hệ feature flag và strangler

Strangler thường đi cùng feature flag để route traffic. Nợ ở đây là flag không retire — nhớ quota xoá cờ như bài đó.


12. Anti-pattern khi trả nợ

  • Refactor “big bang” không metric.
  • Viết lại vì ghét code cũ, không vì SLO/feature bị chặn.
  • Trả nợ không có test — chỉ di chuyển bug.

Bài tập (2 trục)

Vẽ ma trận 2D cho một subsystem thật:

  • Trục X: local ↔ systemic
  • Trục Y: deliberate ↔ accidental

Chọn một ô “systemic + accidental” và đề xuất một bước nhỏ trong 2 tuần (không phải rewrite).

Mở rộng: Viết một đoạn “pitch” 150 từ cho PM từ ma trận đó.


13. Nợ kiến trúc vs nợ implementation

Implementation: code lộn xộn nhưng boundary rõ — refactor an toàn trong module.

Kiến trúc: sai coupling giữa bounded context — cần thay đổi contract, migration dữ liệu, hoặc tách service. Nhầm hai loại dễ dẫn tới “refactor 2 tuần” không giải quyết được vì sai levers.


14. “Not invented here” cũng là nợ

Giữ framework nội bộ khi ecosystem đã có giải pháp chuẩn có thể là nợ deliberate (kiểm soát) hoặc accidental (tự cao). Đánh giá định kỳ: chi phí duy trì framework vs lợi ích.


15. Đo năng suất sau khi trả một phần nợ

Sau initiative, so sánh:

  • median thời gian merge PR tại vùng đó,
  • số incident liên quan,
  • thời gian onboarding (survey định tính cũng có giá trị).

Nếu metric không đổi, cần hỏi lại: đã trả đúng nợ chưa hay chỉ “đẹp code”?


16. Thuật ngữ nhanh

  • Strangler: thay thế dần bề mặt.
  • Freeze surface: ngừng mở rộng API/schema cũ.
  • Interest: chi phí kéo dài do nợ tồn tại.

Tóm tắt cho người vội

  • Phân loại nợ → chọn strangler / freeze / incremental.
  • Đo interest bằng tần suất chạm + sợ hãi + incident.
  • PM cần ngôn ngữ rủi ro/chi phí, không cần drama.

17. “Debt week” có hiệu quả không?

Một tuần chỉ trả nợ có thể hiệu quả nếu trước đó đã có danh sách đã chốt owner + metric; nếu không, dễ thành tuần “refactor lung tung”. Chuẩn bị tối thiểu: 3 initiative nhỏ đo được + 1 initiative rủi ro cao có rollback.


Đọc thêm

  • Ward Cunningham — bài gốc về “technical debt” (meta)
  • Software Engineering at Google — maintenance & deprecation chapters
  • Michael Feathers — Working Effectively with Legacy Code (kỹ thuật tiếp cận)
  • Transaction boundary — khi nợ nằm ở coupling phân tán