Mở đầu — vì sao bài này quan trọng
Hãy tưởng tượng bạn là engineering manager của một startup fintech ở TP.HCM. Sản phẩm chạy tốt, khách hàng tăng, nhưng mỗi lần release một tính năng nhỏ, team lại mất gấp đôi thời gian so với năm ngoái. Bug xuất hiện ở những chỗ tưởng như không liên quan. Dev senior thở dài: "Đụng vào chỗ này là sợ, không ai dám sửa." Còn ban lãnh đạo thì hỏi: "Sao trước làm nhanh mà giờ chậm thế?"
Đó chính là hình ảnh sống động của tech debt (nợ kỹ thuật) khi nó không được quản lý. Tech debt không phải là "code xấu do dev lười". Nó là một khái niệm kinh tế: cái giá phải trả trong tương lai vì đã chọn giải pháp nhanh-tiện ở hiện tại thay vì giải pháp đúng-bền vững hơn.
Vấn đề lớn nhất với tech debt không phải bản thân nó — mọi tổ chức đang phát triển đều có nợ kỹ thuật, và một lượng nợ hợp lý thậm chí còn có lợi. Vấn đề là hầu hết các team không nhìn thấy nó. Nó vô hình trên bảng cân đối, không nằm trong sprint board, không ai "sở hữu" nó. Kết quả là nó âm thầm tích lũy cho đến khi tốc độ giao hàng của cả tổ chức bị bóp nghẹt.
Ở quy mô nhỏ, bạn có thể "sống chung" với nợ. Nhưng khi team scale lên hàng chục, hàng trăm engineer, tech debt trở thành thứ thuế vô hình đánh lên mọi quyết định. Bài này dạy bạn cách nhìn thấy, đo lường, ưu tiên và trả nợ kỹ thuật một cách có hệ thống — biến nó từ nỗi sợ mơ hồ thành một danh mục được quản lý như bất kỳ khoản đầu tư nào.
Khái niệm cốt lõi
Định nghĩa và phép ẩn dụ của Ward Cunningham
Thuật ngữ "technical debt" do Ward Cunningham đặt ra năm 1992. Ông ví việc viết code với việc vay nợ tài chính:
> Việc phát hành code lần đầu cũng giống như đi vay tiền. Một khoản nợ nhỏ giúp bạn tăng tốc phát triển, miễn là nó được trả lại kịp thời bằng cách viết lại (rewrite)... Nguy hiểm xảy ra khi khoản nợ không được trả. Mỗi phút bỏ ra với code chưa đúng được tính như lãi suất trên khoản nợ đó.
Điểm cốt lõi trong phép ẩn dụ là lãi suất (interest). Khi bạn chọn giải pháp nhanh, bạn "vay" thời gian ở hiện tại. Nhưng mỗi lần sau đó phải làm việc với đoạn code đó — sửa bug, thêm tính năng, onboard người mới — bạn trả thêm một chút "lãi": chậm hơn, dễ sai hơn. Nếu không bao giờ "trả gốc" (refactor lại cho đúng), lãi cứ chồng lãi cho đến khi phần lớn năng lực của team bị dùng chỉ để trả lãi thay vì tạo giá trị mới.
Một hiểu lầm quan trọng cần đính chính: Cunningham nói tech debt trong ẩn dụ gốc của ông là nợ có chủ đích và có trách nhiệm — bạn ship sớm để học từ thị trường, rồi quay lại sửa khi đã hiểu rõ hơn. Nó KHÔNG phải là biện minh cho việc viết code cẩu thả. "Viết code lộn xộn vì thiếu hiểu biết" thì không phải nợ, đó đơn giản là làm ẩu.
Ma trận Martin Fowler: 4 loại nợ
Martin Fowler đưa ra một ma trận 2x2 giúp phân loại nợ theo hai trục: cố ý vs vô ý (deliberate vs inadvertent) và thận trọng vs liều lĩnh (prudent vs reckless):
- Cố ý + Thận trọng: "Ta biết đây chưa phải cách tốt nhất, nhưng cần ship kịp deadline; sẽ quay lại sửa." — Đây là nợ tốt, nợ chiến lược.
- Cố ý + Liều lĩnh: "Không có thời gian cho thiết kế, cứ code đại." — Nợ nguy hiểm, thường trả giá đắt.
- Vô ý + Thận trọng: "Bây giờ nhìn lại mới biết đáng ra nên thiết kế thế này." — Nợ từ sự học hỏi, không thể tránh hoàn toàn.
- Vô ý + Liều lĩnh: "Layering là gì?" — Nợ do thiếu năng lực, cần đầu tư đào tạo.
Các dạng tech debt phổ biến
Nợ kỹ thuật không chỉ nằm ở code. Trong thực tế nó xuất hiện ở nhiều lớp:
- Code debt: hàm dài 500 dòng, copy-paste, đặt tên khó hiểu, thiếu abstraction.
- Architecture debt: quyết định kiến trúc không còn phù hợp với quy mô hiện tại (ví dụ monolith cần tách nhưng chưa tách).
- Test debt: thiếu test tự động, khiến mỗi thay đổi đều rủi ro.
- Documentation debt: kiến thức nằm trong đầu một vài người, không viết ra.
- Dependency debt: thư viện, framework, phiên bản ngôn ngữ đã cũ, không còn được hỗ trợ (còn gọi là "software rot" hay bit rot).
- Infrastructure debt: cấu hình thủ công, không có Infrastructure-as-Code, deploy bằng tay.
Phân biệt tech debt với bug và với "làm ẩu"
Cần nắm rõ ba khái niệm không phải là một:
- Bug là hành vi sai của phần mềm — cần sửa để đúng.
- Tech debt là code/kiến trúc hoạt động đúng nhưng khó thay đổi, tạo ma sát cho công việc tương lai.
- Làm ẩu (sloppiness) là nợ liều lĩnh vô ích, không mang lại lợi ích đánh đổi nào.
Tình huống thực tế
Ví dụ 1 — Tiki và cuộc "tái cấu trúc để scale" (bối cảnh giả định hợp lý)
Một sàn thương mại điện tử lớn tại Việt Nam khởi đầu bằng một monolith PHP duy nhất. Trong 3 năm đầu, tốc độ ship tính năng cực nhanh — đúng như lợi ích của "vay nợ" sớm. Nhưng đến khi lưu lượng vào mùa sale (11/11, 12/12) tăng gấp 10 lần, hệ thống bắt đầu sập từng phần. Mỗi lần thêm tính năng cho phần thanh toán lại vô tình làm hỏng phần tìm kiếm, vì mọi thứ dính chặt vào nhau.
Diễn giải: Đây là architecture debt điển hình. Quyết định monolith ban đầu là nợ thận trọng, cố ý — hoàn toàn đúng cho giai đoạn tìm product-market fit. Nhưng "lãi suất" tăng vọt khi quy mô thay đổi. Team đã dành nguyên một quý để tách các domain quan trọng (thanh toán, kho, tìm kiếm) thành service riêng, đồng thời viết integration test cho phần lõi. Sau đợt tách, thời gian trung bình để release một thay đổi ở module thanh toán giảm từ 5 ngày xuống còn 1 ngày.
Bài học: Nợ tốt ở giai đoạn này có thể trở thành nợ độc ở giai đoạn khác. Đừng đánh giá một quyết định kiến trúc là "sai" mà không xét bối cảnh nó được đưa ra. Nhưng phải chủ động trả nợ trước khi lãi suất bóp nghẹt tốc độ.
Ví dụ 2 — Startup logistics và "quy tắc 20%"
Một startup logistics ở Singapore, khoảng 40 engineer, nhận ra tốc độ giao hàng tính năng giảm 30% trong 6 tháng dù đã tuyển thêm người. Khảo sát nội bộ (developer survey) cho thấy 68% engineer nói họ "thường xuyên bị chậm bởi code cũ khó hiểu". CTO quyết định áp dụng chính sách rõ ràng: mỗi sprint dành 20% capacity cho việc trả nợ kỹ thuật, không thương lượng. Họ lập một "tech debt register" trên Jira, mỗi khoản nợ được gắn nhãn ước lượng "lãi suất" (bao lâu nó làm chậm team mỗi tháng).
Sau 2 quý, chỉ số DORA "lead time for changes" cải thiện 40%, và điểm survey về "bị chậm bởi code cũ" giảm còn 35%.
Diễn giải: Điểm mấu chốt không phải con số 20% (con số này tùy tổ chức), mà là biến việc trả nợ thành một khoản ngân sách cố định và được bảo vệ. Khi không có ngân sách rõ ràng, việc trả nợ luôn thua trong cuộc đua ưu tiên với tính năng mới — vì tính năng mới luôn "khẩn cấp" hơn.
Bài học: Trả nợ liên tục theo tỷ lệ đều đặn hiệu quả hơn nhiều so với những đợt "refactor lớn" hoành tráng rồi bỏ dở. Nợ cần được trả góp, không phải trả một lần.
Ví dụ 3 — Cái bẫy "Big Rewrite" của một công ty SaaS
Một công ty SaaS (bối cảnh giả định) chán ngán codebase cũ đến mức quyết định "viết lại toàn bộ từ đầu" — một dự án rewrite kéo dài dự kiến 6 tháng. Thực tế nó kéo dài 18 tháng, đội chi phí gấp 3, trong khi hệ thống cũ vẫn phải maintain song song. Tệ hơn, phiên bản mới lặp lại nhiều lỗi mà hệ thống cũ đã học được cách xử lý qua nhiều năm (những "business rule ngầm" không ai viết ra).
Diễn giải: "Big rewrite" là một trong những cái bẫy tốn kém nhất khi xử lý tech debt. Joel Spolsky từng gọi việc viết lại từ đầu là "sai lầm chiến lược tồi tệ nhất mà một công ty phần mềm có thể mắc". Lý do: bạn vứt bỏ toàn bộ tri thức đã tích lũy trong code cũ, và trong lúc đó đối thủ vẫn tiến lên.
Bài học: Phần lớn tech debt nên được trả bằng phương pháp refactoring dần dần (incremental) — như kỹ thuật "Strangler Fig" của Martin Fowler: xây dần phần mới bao quanh phần cũ rồi rút phần cũ ra từng mảnh. Rewrite toàn bộ chỉ nên là lựa chọn cuối cùng, cho những trường hợp thực sự đặc biệt.
Hướng dẫn từng bước
Dưới đây là quy trình thực tế để quản lý tech debt ở quy mô team:
Bước 1 — Làm nợ trở nên hữu hình (Track). Bạn không thể quản lý thứ bạn không thấy. Lập một tech debt register — có thể là một board/label riêng trên Jira, Linear, hoặc một trang Notion. Mỗi khoản nợ ghi rõ: nó là gì, nó gây ma sát ở đâu, tác động (lãi suất) ra sao, và chi phí ước lượng để trả. Khuyến khích engineer tạo ticket nợ ngay khi họ gặp nó, thay vì để trong đầu.
Bước 2 — Định lượng "lãi suất". Với mỗi khoản nợ, ước lượng nó làm chậm team bao nhiêu. Ví dụ: "module này khiến mỗi thay đổi tốn thêm 2 ngày, và ta đụng vào nó khoảng 3 lần/tháng → 6 ngày/tháng lãi." Con số này biến nợ trừu tượng thành ngôn ngữ kinh doanh.
Bước 3 — Ưu tiên theo lãi suất × tần suất. Không phải mọi khoản nợ đều đáng trả. Nợ ở đoạn code không ai đụng tới thì lãi suất bằng 0 — cứ để đó. Ưu tiên trả những khoản có lãi suất cao và nằm ở vùng thay đổi thường xuyên. (Bài về prioritization frameworks sẽ đi sâu hơn về công cụ; ở đây bạn dùng logic đơn giản: tác động cao, tần suất cao.)
Bước 4 — Phân bổ ngân sách cố định. Cam kết một tỷ lệ capacity đều đặn cho việc trả nợ — thường 15–20% mỗi sprint. Bảo vệ ngân sách này khỏi bị "nuốt" bởi feature mới. Đây là chìa khóa quan trọng nhất.
Bước 5 — Ưu tiên trả nợ theo kiểu "boy scout" và cơ hội. Áp dụng nguyên tắc Boy Scout Rule: "để lại code sạch hơn lúc bạn tìm thấy nó." Khi đụng vào một vùng code để làm tính năng, tranh thủ dọn dẹp một chút xung quanh. Refactoring cơ hội (opportunistic) này trả nợ đúng chỗ đang có hoạt động.
Bước 6 — Ngăn nợ mới bằng "definition of done". Đưa các tiêu chuẩn (test coverage, code review, cập nhật docs) vào định nghĩa "hoàn thành" của mỗi task. Nợ dễ phòng hơn dễ trả.
Bước 7 — Báo cáo cho ban lãnh đạo bằng ngôn ngữ kinh doanh. Đừng nói "code xấu". Nói: "Khoản nợ ở hệ thống thanh toán đang khiến tốc độ ship chậm 30% và tăng rủi ro sự cố mùa sale; đầu tư 3 tuần trả nợ sẽ phục hồi tốc độ." Gắn nợ với velocity, rủi ro, và chi phí tuyển dụng (nợ cao làm engineer nản và nghỉ việc).
Lỗi thường gặp & mẹo
Lỗi 1 — Coi mọi tech debt là xấu. Một số nợ là quyết định kinh doanh khôn ngoan. Ship sớm để học thị trường rồi sửa sau là chiến lược đúng. Mục tiêu không phải là "nợ = 0" mà là nợ có chủ đích và được quản lý.
Lỗi 2 — Chờ "khi nào rảnh sẽ dọn". Sẽ không bao giờ rảnh. Nếu không có ngân sách cố định, việc trả nợ luôn thua. Mẹo: biến nó thành lịch trình mặc định, không phải quyết định phải bàn lại mỗi sprint.
Lỗi 3 — Refactor mà không có test bao phủ. Refactor code cũ không có test là đi trên dây không lưới. Mẹo: viết "characterization test" (test mô tả hành vi hiện tại) trước, rồi mới refactor an toàn.
Lỗi 4 — Big rewrite thay vì incremental. Như ví dụ 3, viết lại từ đầu thường là bẫy chi phí. Mẹo: dùng Strangler Fig, thay thế dần từng phần.
Lỗi 5 — Không có ai "sở hữu" nợ. Nếu nợ là việc của "mọi người" thì thành việc của "không ai". Mẹo: gán chủ sở hữu cho từng khoản nợ lớn, hoặc để mỗi team sở hữu nợ trong domain của mình.
Lỗi 6 — Không đo lường để biết có tiến bộ. Mẹo: theo dõi các chỉ số như lead time, tần suất bug ở vùng nợ, hoặc điểm developer survey về ma sát. Nếu trả nợ mà velocity không cải thiện, có thể bạn đang trả nhầm khoản.
Mẹo bổ sung — Dùng công cụ tự động. SonarQube, CodeScene có thể chỉ ra "hotspot" — nơi code phức tạp và thay đổi thường xuyên. Đây thường là ứng viên trả nợ có ROI cao nhất.
Bài tập thực hành
- Lập tech debt register. Chọn một codebase bạn đang làm việc. Liệt kê 5–7 khoản tech debt bạn biết. Với mỗi khoản, ghi: mô tả, loại nợ (code/architecture/test/dependency...), và ước lượng "lãi suất" (nó làm chậm bạn bao nhiêu mỗi tháng).
- Phân loại theo ma trận Fowler. Đặt mỗi khoản nợ vào một trong 4 ô của ma trận Fowler (cố ý/vô ý × thận trọng/liều lĩnh). Bạn thấy phần lớn nợ của mình rơi vào ô nào? Điều đó nói gì về tổ chức của bạn?
- Ưu tiên và đề xuất ngân sách. Sắp xếp 5–7 khoản nợ theo (lãi suất × tần suất chạm tới). Chọn top 2 và viết một đoạn 3–4 câu thuyết phục ban lãnh đạo đầu tư trả nợ — hoàn toàn bằng ngôn ngữ kinh doanh (velocity, rủi ro, chi phí), không dùng từ ngữ kỹ thuật thuần túy.
- Thiết kế chính sách. Viết một chính sách 1 trang cho team về cách quản lý tech debt: tỷ lệ capacity dành cho nợ, cách track, ai sở hữu, và tiêu chí "definition of done" để ngăn nợ mới.
Tóm tắt
- Tech debt là khái niệm kinh tế, không phải lời chê đạo đức: cái giá tương lai của việc chọn nhanh-tiện thay vì đúng-bền ở hiện tại. Phép ẩn dụ của Ward Cunningham nhấn mạnh lãi suất — chi phí liên tục khi làm việc với code chưa đúng.
- Không phải mọi nợ đều xấu. Ma trận Fowler (cố ý/vô ý × thận trọng/liều lĩnh) giúp phân biệt nợ chiến lược khôn ngoan với nợ do làm ẩu hay thiếu năng lực.
- Nợ tồn tại ở nhiều lớp: code, kiến trúc, test, docs, dependency, hạ tầng. Và nó khác với bug (hành vi sai) và làm ẩu (nợ vô ích).
- Quản lý nợ theo quy trình: track (làm hữu hình) → định lượng lãi suất → ưu tiên theo lãi × tần suất → ngân sách cố định 15–20% → trả góp liên tục thay vì big rewrite.
- Tránh các bẫy kinh điển: coi mọi nợ là xấu, chờ "khi rảnh", refactor không test, big rewrite, và không ai sở hữu nợ.
- Ở quy mô lớn, tech debt là thuế vô hình lên mọi quyết định. Team thắng là team biến nó thành danh mục được quản lý — nhìn thấy, đo được, và trả một cách kỷ luật.