Mở đầu — vì sao bài này quan trọng
Hãy tưởng tượng bạn vừa được thăng lên Tech Lead của một đội 6 người tại một công ty fintech ở TP.HCM. Trong buổi họp roadmap quý, bạn nói với sếp: "Team cần 3 tuần để trả nợ kỹ thuật trước khi làm feature mới." Sếp nhìn bạn, cau mày, và hỏi một câu mà bạn không trả lời được: "Nợ kỹ thuật là gì? Nó tốn bao nhiêu tiền của công ty mỗi tháng? Nếu không trả thì sao?"
Đây là khoảnh khắc phân định giữa một kỹ sư giỏi và một người dẫn dắt kỹ thuật thực thụ. Kỹ sư nhìn nợ kỹ thuật như "code xấu cần dọn". Còn Tech Lead phải nhìn nó như một khoản nợ tài chính — có lãi suất, có kỳ hạn, có loại nợ tốt và nợ xấu, và quan trọng nhất: có thể được quản lý một cách có chủ đích thay vì né tránh trong sợ hãi.
Nợ kỹ thuật (technical debt) là một trong những chủ đề bị hiểu sai nhiều nhất trong ngành. Nhiều người dùng nó như cái cớ để chê bai code của người khác, hoặc như lý do để đòi viết lại toàn bộ hệ thống. Cả hai đều sai. Trong bài này, bạn sẽ học cách phân loại nợ kỹ thuật một cách chính xác, và xây dựng một hệ thống quản lý để nó không âm thầm bóp nghẹt tốc độ của team. Đây là kỹ năng lãnh đạo cốt lõi — bởi vì quản lý nợ kỹ thuật thực chất là quản lý sự đánh đổi giữa tốc độ hôm nay và tốc độ ngày mai.
Khái niệm cốt lõi
Định nghĩa gốc của Ward Cunningham
Người phát minh ra thuật ngữ "technical debt" là Ward Cunningham — chính là người tạo ra wiki đầu tiên trên thế giới. Điều bất ngờ là định nghĩa gốc của ông hoàn toàn khác với cách đa số người dùng ngày nay.
Cunningham nói đại ý: "Ship code đầu tiên cũng giống như đi vay tiền. Việc vay là ổn, miễn là bạn trả lại bằng cách viết lại (refactor) khi bạn đã học được thêm về vấn đề. Nguy hiểm xảy ra khi bạn không bao giờ trả nợ đó."
Điểm mấu chốt mà ít người nhớ: Nợ kỹ thuật KHÔNG phải là code xấu. Trong tư duy của Cunningham, nợ kỹ thuật là quyết định cố ý ship một phiên bản chưa hoàn hảo nhưng phản ánh đúng hiểu biết hiện tại của bạn, để nhanh chóng đưa ra thị trường và học từ thực tế. Sau khi học được, bạn quay lại và cập nhật code cho khớp với hiểu biết mới.
Nói cách khác, nợ kỹ thuật đúng nghĩa là một công cụ chiến lược: bạn cố tình đi vay "thời gian tương lai" để có "tốc độ hiện tại". Code cẩu thả do thiếu năng lực, viết ẩu vì lười — đó không phải nợ, đó chỉ là kém chất lượng. Phân biệt được hai thứ này là bước đầu tiên để trưởng thành với vai trò lãnh đạo.
Bốn góc phần tư của Martin Fowler
Martin Fowler đưa ra một khung phân loại kinh điển gọi là Technical Debt Quadrant, chia nợ theo hai trục: cố ý / vô ý (deliberate / inadvertent) và thận trọng / liều lĩnh (prudent / reckless). Bốn ô tạo thành:
- Cố ý + Thận trọng: "Ta không có thời gian làm đúng ngay bây giờ, ta ship trước và sẽ quay lại." Đây là nợ tốt — đúng tinh thần Cunningham.
- Cố ý + Liều lĩnh: "Không có thời gian cho thiết kế, cứ code đi." Đây là nợ xấu vì bạn biết mình đang làm ẩu nhưng vẫn kệ.
- Vô ý + Thận trọng: "Bây giờ nhìn lại, đáng ra ta nên thiết kế thế này." Đây là loại nợ không tránh khỏi — bạn chỉ hiểu ra sau khi đã làm. Đây thực ra là loại lành mạnh nhất và phổ biến nhất.
- Vô ý + Liều lĩnh: "Layered architecture là gì?" Team không biết mình không biết. Đây là loại nguy hiểm nhất vì nó ẩn.
Lãi suất: khái niệm quan trọng nhất
Trong tài chính, thứ giết bạn không phải khoản vay gốc, mà là lãi suất. Nợ kỹ thuật cũng vậy. "Lãi suất" của nợ kỹ thuật chính là chi phí phụ trội bạn phải trả mỗi khi làm việc với vùng code đó: mất thêm thời gian đọc hiểu, dễ gây bug, phải viết workaround, khó onboard người mới.
Một khoản nợ có lãi suất thấp — ví dụ một module xấu nhưng gần như không ai động vào — có thể để yên mãi mãi mà không sao. Ngược lại, một khoản nợ nằm ngay giữa "hot path" mà cả team đụng vào hàng ngày thì lãi suất cực cao, phải trả gấp. Ưu tiên trả nợ theo lãi suất, không phải theo mức độ xấu. Đây là nguyên tắc số một mà tôi muốn bạn khắc cốt ghi tâm.
Nợ kỹ thuật là vô hình — và đó là vấn đề
Nợ tài chính hiện trên bảng cân đối kế toán. Nợ kỹ thuật thì không. Nó ẩn trong codebase, không ai thấy cho đến khi feature bị chậm, bug tăng vọt, hoặc kỹ sư giỏi nghỉ việc vì "code kinh khủng quá". Nhiệm vụ của Tech Lead là làm cho nợ trở nên hữu hình — ghi lại, đo lường, và đưa nó vào cuộc trò chuyện với business bằng ngôn ngữ họ hiểu: thời gian, tiền, và rủi ro.
Tình huống thực tế
Tình huống 1: Startup gọi xe Đông Nam Á và cú đánh đổi có chủ đích
Một startup gọi xe (gọi tên giả định là "GoRide") ở giai đoạn đầu tại Jakarta cần ra mắt tính năng thanh toán ví điện tử trước Tết để cạnh tranh. Đội kỹ thuật biết rằng thiết kế đúng đắn cần một service thanh toán riêng, có hàng đợi, có retry logic. Nhưng làm đúng mất 8 tuần, mà thị trường chỉ cho họ 3 tuần.
Tech Lead ra quyết định: ship một phiên bản "đơn giản hóa" — xử lý thanh toán đồng bộ ngay trong monolith, hard-code một số quy tắc, và ghi rõ trong một tài liệu "Debt Register": "Payment xử lý đồng bộ, sẽ nghẽn khi vượt 200 giao dịch/phút. Deadline trả nợ: Q2, khi đạt 5.000 user active." Họ ship đúng hạn, chiếm thị phần.
Diễn giải: Đây là nợ cố ý + thận trọng đúng sách. Điểm mấu chốt không phải là họ đi tắt, mà là họ đi tắt có ý thức, có ghi chép, có điều kiện kích hoạt trả nợ. Đến Q2, khi lưu lượng thật sự tăng, họ đã có sẵn kế hoạch tách service.
Bài học: Nợ tốt luôn đi kèm một "biên nhận nợ". Nếu bạn đi tắt mà không ghi lại điều kiện phải quay lại trả, bạn không đang vay có kế hoạch — bạn đang tự lừa mình.
Tình huống 2: Ngân hàng số và khoản nợ vô hình phát nổ
Một đội tại một ngân hàng số ở Việt Nam kế thừa một module tính lãi suất được viết cách đây 3 năm bởi các kỹ sư đã nghỉ. Không ai hiểu hết logic, không có test, comment bằng tiếng Việt lẫn tiếng Anh, và có những if lồng nhau 7 tầng. Mỗi lần cần thêm một loại sản phẩm vay mới, một kỹ sư mất trung bình 2 tuần chỉ để dò xem thay đổi có làm hỏng gì không, và cứ 3 lần thì 1 lần gây bug lên production.
Tech Lead làm một việc đơn giản nhưng đắt giá: bà đo lượng thời gian team tiêu vào module này trong 3 tháng. Con số: khoảng 40% năng lực của 2 kỹ sư, tương đương gần 1 tỷ đồng chi phí nhân sự mỗi năm chỉ để "vật lộn" với module này. Bà mang con số đó cho ban lãnh đạo, và lần đầu tiên việc "viết lại module lãi suất" được duyệt như một dự án có ROI rõ ràng.
Diễn giải: Đây là nợ vô ý + liều lĩnh — không ai chủ động tạo ra nó, nhưng nó tồn tại và có lãi suất khổng lồ vì nằm ngay hot path. Điều biến nó từ "kêu ca" thành "dự án được duyệt" chính là việc định lượng lãi suất bằng tiền và thời gian thật.
Bài học: Business không phản ứng với "code xấu". Họ phản ứng với "chúng ta đang mất 1 tỷ/năm". Hãy dịch nợ kỹ thuật sang ngôn ngữ của tiền.
Tình huống 3: Cái bẫy "viết lại toàn bộ"
Một công ty thương mại điện tử ở Hà Nội có một team mới, đầy nhiệt huyết. Họ nhìn hệ thống cũ, thốt lên "codebase này quá tệ, phải viết lại từ đầu bằng microservices". Ban lãnh đạo đồng ý. 14 tháng sau, hệ thống mới vẫn chưa đạt được tính năng ngang hệ thống cũ, trong khi đối thủ đã ra 5 tính năng mới. Cuối cùng dự án viết lại bị hủy, hai kỹ sư senior nghỉ việc.
Diễn giải: Đây là sai lầm kinh điển gọi là "second-system effect". Họ nhầm lẫn giữa trả nợ (refactor từng phần, an toàn) và tuyên bố phá sản rồi xây lại thành phố (rewrite toàn bộ, rủi ro khủng khiếp). Hệ thống cũ dù xấu vẫn chứa hàng nghìn quyết định business đúng đắn tích lũy qua nhiều năm mà không ai document — viết lại là ném hết đi.
Bài học: Rewrite toàn bộ gần như luôn là quyết định tồi. Chiến lược đúng gần như luôn là strangler fig pattern — bọc dần hệ thống cũ, thay từng phần nhỏ, giữ hệ thống luôn chạy được.
Hướng dẫn từng bước
Đây là quy trình 6 bước để xây dựng một hệ thống quản lý nợ kỹ thuật cho team của bạn:
Bước 1 — Lập Debt Register (sổ nợ). Tạo một nơi tập trung (Notion, một file TECH_DEBT.md trong repo, hoặc backlog có tag riêng) để ghi mọi khoản nợ. Mỗi mục cần: mô tả, vị trí trong code, lý do phát sinh, và ước lượng lãi suất. Nợ được viết ra là nợ có thể quản lý; nợ trong đầu người ta là nợ mất kiểm soát.
Bước 2 — Phân loại theo Fowler. Với mỗi khoản, gán nó vào một trong bốn góc phần tư. Điều này giúp bạn quyết định thái độ: nợ vô ý-thận trọng thì chấp nhận và học; nợ vô ý-liều lĩnh thì cần training và nâng chuẩn team.
Bước 3 — Ước lượng lãi suất, không phải nợ gốc. Với mỗi khoản, hỏi: "Vùng code này bị đụng vào bao nhiêu lần mỗi tháng? Mỗi lần tốn thêm bao lâu vì nó?" Nhân hai số lại. Đó là lãi suất hàng tháng. Ưu tiên xử lý theo con số này.
Bước 4 — Đặt ngân sách trả nợ cố định. Nguyên tắc thực dụng nhất: dành một tỷ lệ cố định năng lực mỗi sprint cho việc trả nợ — thường là 15–20%. Đừng chờ "khi nào rảnh" vì sẽ không bao giờ rảnh. Biến nó thành một hằng số văn hóa, giống như bạn luôn trả góp một khoản nợ đều đặn mỗi tháng.
Bước 5 — Gắn trả nợ vào công việc feature (Boy Scout Rule). Áp dụng nguyên tắc hướng đạo sinh: "Luôn để chỗ cắm trại sạch hơn lúc bạn đến." Khi ai đó động vào một vùng code có nợ để làm feature, họ dọn dẹp một chút vùng đó. Cách này trả nợ đúng nơi lãi suất cao nhất mà không cần dự án riêng.
Bước 6 — Ngăn chặn tại nguồn (Definition of Done). Nợ tốt nhất là nợ không phát sinh. Đưa các tiêu chuẩn vào "Definition of Done": có test, qua review, không copy-paste logic. Điều này chặn dòng nợ vô ý-liều lĩnh mới đổ vào.
Lỗi thường gặp & mẹo
Lỗi 1 — Dùng "nợ kỹ thuật" như từ chê bai. Nhiều kỹ sư gọi mọi code họ không thích là "nợ kỹ thuật". Điều này làm loãng khái niệm và tạo văn hóa đổ lỗi. Mẹo: Yêu cầu mọi người khi nói "nợ" phải chỉ ra được lãi suất cụ thể — nó khiến ai chậm lại, ở đâu, bao nhiêu.
Lỗi 2 — Đòi "dừng feature để trả hết nợ". Không bao giờ có chuyện trả hết nợ, cũng như không ai sống mà không có chút nợ nào. Mẹo: Đặt mục tiêu giữ nợ ở mức bền vững, không phải về 0. Ngân sách 15–20% đều đặn hiệu quả hơn nhiều so với các "đợt trả nợ" khủng hoảng.
Lỗi 3 — Trả nợ ở nơi lãi suất thấp. Kỹ sư thích dọn dẹp module họ ghét, dù chẳng ai đụng tới nó. Đó là lãng phí. Mẹo: Luôn hỏi "vùng này có nằm trên hot path không?" trước khi bỏ công refactor.
Lỗi 4 — Không định lượng khi nói chuyện với business. "Code xấu lắm" không thuyết phục được ai giữ ngân sách. Mẹo: Luôn quy đổi ra giờ-người và tiền, như tình huống ngân hàng số ở trên.
Lỗi 5 — Nhầm rewrite với refactor. Mẹo: Mặc định chọn refactor từng phần và strangler fig. Chỉ cân nhắc rewrite khi công nghệ nền đã thật sự chết (ví dụ ngôn ngữ hết hỗ trợ) và có kế hoạch di trú rõ ràng.
Mẹo vàng: Làm nợ hữu hình. Một dashboard đơn giản hiển thị "top 5 vùng nợ có lãi suất cao nhất tháng này" tạo ra sự minh bạch mạnh hơn mọi bài phàn nàn.
Bài tập thực hành
- Lập Debt Register cho một hệ thống bạn đang làm. Liệt kê ít nhất 5 khoản nợ kỹ thuật. Với mỗi khoản, ghi: mô tả, vị trí, và phân loại theo bốn góc phần tư của Fowler.
- Tính lãi suất. Chọn khoản nợ mà bạn cho là tệ nhất. Ước lượng: nó bị đụng vào bao nhiêu lần/tháng, mỗi lần tốn thêm bao nhiêu giờ. Quy đổi ra tiền (dùng chi phí giờ-người của công ty bạn). Bạn có bất ngờ với con số không?
- Sắp xếp lại ưu tiên. So sánh thứ tự "khoản nợ xấu nhất theo cảm giác" với thứ tự "khoản nợ có lãi suất cao nhất". Chúng khác nhau ở đâu? Điều đó nói gì về nơi bạn nên dồn công sức?
- Viết một "biên nhận nợ". Giả sử tuần tới bạn phải đi tắt để ship kịp deadline. Hãy viết trước một mục Debt Register cho khoản nợ đó, kèm điều kiện kích hoạt trả nợ (ví dụ: "khi lượng user vượt X" hoặc "trước sprint Y").
- Bài tập lãnh đạo: Soạn một đoạn 5 câu để trình bày với sếp phi kỹ thuật về nhu cầu trả một khoản nợ cụ thể — chỉ dùng ngôn ngữ tiền, thời gian, và rủi ro, tuyệt đối không dùng thuật ngữ kỹ thuật.
Tóm tắt
Nợ kỹ thuật không phải code xấu — nó là một quyết định đánh đổi giữa tốc độ hôm nay và tốc độ ngày mai, đúng như Ward Cunningham đã định nghĩa. Dùng khung bốn góc phần tư của Fowler để phân biệt nợ tốt (cố ý, thận trọng, có ghi chép) với nợ nguy hiểm (vô ý, liều lĩnh, ẩn giấu). Điều quan trọng nhất không phải nợ gốc mà là lãi suất — hãy ưu tiên trả nợ ở nơi cả team đụng vào nhiều nhất, chứ không phải ở nơi code xấu nhất.
Về mặt quản lý: lập Debt Register để làm nợ hữu hình, định lượng lãi suất bằng tiền để nói chuyện được với business, dành một ngân sách trả nợ cố định 15–20% mỗi sprint, áp dụng Boy Scout Rule để trả nợ đúng chỗ, và ngăn nợ mới tại nguồn qua Definition of Done. Tránh cái bẫy "viết lại toàn bộ" — hầu như luôn chọn refactor từng phần theo strangler fig pattern.
Là một Tech Lead, giá trị của bạn không nằm ở việc viết code sạch nhất, mà ở việc quản lý sự đánh đổi kỹ thuật một cách có chủ đích để team đi nhanh bền vững qua nhiều năm — chứ không phải phóng nhanh vài tháng rồi mắc kẹt.