Menu
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Đang tải...

Tech Debt và Architecture Decisions

Technical Leadership Bài 2/60

Mở đầu — vì sao bài này quan trọng

Nếu bạn hỏi bất kỳ Tech Lead nào về nỗi đau âm ỉ nhất trong công việc của họ, gần như chắc chắn hai từ "tech debt" (nợ kỹ thuật) sẽ được nhắc đến. Nợ kỹ thuật không giết chết sản phẩm trong một ngày. Nó giết chết đội ngũ của bạn theo cách chậm rãi và âm thầm: mỗi tuần code chậm hơn một chút, mỗi lần deploy hồi hộp hơn một chút, mỗi kỹ sư giỏi rời đi vì "chán làm việc với đống code này".

Điều nguy hiểm là phần lớn Tech Lead xử lý tech debt bằng cảm tính. Họ hoặc phủ nhận nó ("code chạy được là ổn rồi, đừng có cầu toàn"), hoặc rơi vào thái cực ngược lại — đòi refactor toàn bộ, dừng feature vài tháng để "làm cho đúng". Cả hai đều sai, và cả hai đều khiến bạn mất uy tín với business.

Là một Tech Lead, giá trị lớn nhất bạn mang lại không phải là viết code sạch nhất, mà là ra được những quyết định kiến trúc và quyết định về nợ kỹ thuật một cách có chủ đích, có thể giải thích được, và cân bằng được giữa tốc độ giao hàng hôm nay với sức khỏe hệ thống ngày mai. Bài này sẽ cho bạn một khung tư duy để làm điều đó — phân loại nợ, định giá nó, và biết khi nào nên "vay", khi nào nên "trả".

Khái niệm cốt lõi

Tech debt là một phép ẩn dụ tài chính — hãy dùng nó cho đúng

Ward Cunningham, người sáng tạo ra khái niệm này, ví việc viết code nhanh mà chưa hoàn hảo giống như đi vay tiền: bạn có được tốc độ ngay lập tức, nhưng phải trả lãi. "Lãi" ở đây là công sức phụ trội mỗi lần bạn phải làm việc với đoạn code chưa tốt đó. Nếu bạn không bao giờ "trả gốc" (refactor), lãi sẽ chồng chất đến mức mọi thay đổi nhỏ cũng trở nên đắt đỏ.

Điểm mấu chốt mà nhiều người hiểu sai: vay nợ không phải lúc nào cũng xấu. Không có doanh nghiệp lớn nào phát triển mà không vay vốn. Vấn đề không phải là có nợ hay không, mà là bạn có biết mình đang nợ bao nhiêu, lãi suất ra sao, và có kế hoạch trả hay không.

Bốn góc phần tư của Martin Fowler

Martin Fowler mở rộng khái niệm thành một ma trận 2x2 rất hữu ích, dựa trên hai câu hỏi: nợ này là cố ý hay vô tình, và thận trọng hay liều lĩnh.

  • Deliberate & Prudent (Cố ý và thận trọng): "Chúng ta biết đây chưa phải giải pháp tối ưu, nhưng cần ra mắt kịp Tết, và chúng ta chấp nhận sẽ dọn dẹp sau." Đây là loại nợ tốt — một quyết định kinh doanh tỉnh táo.
  • Deliberate & Reckless (Cố ý và liều lĩnh): "Chúng ta không có thời gian cho thiết kế, cứ viết đại đi." Biết rõ đang làm ẩu nhưng vẫn làm mà không cân nhắc hậu quả.
  • Accidental/Inadvertent & Prudent (Vô tình nhưng thận trọng): "Sau khi làm xong, giờ chúng ta mới hiểu lẽ ra nên thiết kế thế này." Đây là loại nợ học được — bạn chỉ nhận ra thiết kế đúng sau khi đã đi qua thiết kế sai. Không thể tránh hoàn toàn.
  • Accidental & Reckless (Vô tình và liều lĩnh): "Layering là gì? Design pattern là gì?" — đội không đủ năng lực để nhận ra mình đang tạo ra mớ hỗn độn.
Sự phân loại này quan trọng vì mỗi loại đòi hỏi một hành động khác nhau. Nợ cố ý-thận trọng cần được ghi chép và lên lịch trả. Nợ vô tình-thận trọng cần được refactor khi bạn "học" được cái đúng. Nợ liều lĩnh (cả hai loại) là dấu hiệu của vấn đề về quy trình hoặc năng lực đội ngũ — bạn phải xử lý gốc rễ, không chỉ dọn hậu quả.

Deliberate vs Accidental — hai nhóm bạn gặp hàng ngày

Trong thực tế vận hành, tôi thường gom lại thành hai nhóm để nói chuyện với đội:

Nợ cố ý (Deliberate): những đường tắt bạn chọn để đánh đổi lấy tốc độ. Ví dụ: hardcode danh sách tỉnh thành thay vì gọi API, bỏ qua viết test cho tính năng thử nghiệm, dùng bảng CSV thay vì database cho MVP. Đây là những quyết định hợp lệ nếu bạn ghi lại và có ý định quay lại.

Nợ vô tình (Accidental): những thiết kế bạn phát hiện ra là sai sau khi hệ thống lớn lên. Ban đầu bạn thiết kế module Order gánh luôn logic thanh toán vì lúc đó chỉ có một hình thức trả tiền. Hai năm sau bạn có ví điện tử, trả góp, COD, mã giảm giá... và cái module Order khổng lồ giờ là ác mộng. Không ai làm sai ở thời điểm đó — bối cảnh đã thay đổi.

Quyết định kiến trúc: nơi nợ được sinh ra

Phần lớn tech debt nghiêm trọng không đến từ code cẩu thả ở cấp dòng lệnh, mà từ những quyết định kiến trúc ở cấp cao: chọn monolith hay microservices, chọn SQL hay NoSQL, có tách service thanh toán riêng hay không, coupling giữa các module ra sao. Một quyết định kiến trúc sai lầm tạo ra loại nợ đắt nhất và khó trả nhất, vì nó ăn sâu vào cấu trúc.

Nguyên tắc vàng ở đây là phân biệt quyết định một chiều (one-way door)quyết định hai chiều (two-way door) của Jeff Bezos. Quyết định hai chiều — dễ đảo ngược — thì hãy quyết nhanh, thử, sai thì sửa. Quyết định một chiều — khó đảo ngược, như việc chọn ngôn ngữ lõi hay cấu trúc dữ liệu nền tảng — thì phải cân nhắc kỹ, vì nợ từ đây rất khó trả.

Tình huống thực tế

Tình huống 1: Startup fintech ở TP.HCM và cú "vay nợ" đúng lúc

Một startup ví điện tử tại TP.HCM (gọi là công ty A) cần ra mắt tính năng thanh toán hóa đơn điện nước trước mùa cao điểm cuối năm. Đội engineering chỉ có 6 người, deadline 5 tuần. Giải pháp "đúng" là tích hợp trực tiếp với API của từng nhà cung cấp điện, nước — nhưng mỗi nhà cung cấp một chuẩn khác nhau, ước tính mất 10 tuần.

Tech Lead của A đưa ra một quyết định cố ý và thận trọng: thay vì tích hợp trực tiếp, họ dùng một nhà tổng hợp trung gian (payment aggregator) với phí cao hơn 0.3% mỗi giao dịch. Điều quan trọng là anh ấy làm ba việc: (1) ghi lại quyết định này trong một tài liệu ngắn nêu rõ đây là nợ tạm thời; (2) tính ra "lãi suất" cụ thể — với dự phóng 50.000 giao dịch/tháng, khoản phí phụ trội là khoảng 45 triệu đồng/tháng; (3) đặt một ngưỡng kích hoạt: "khi vượt 200.000 giao dịch/tháng, chúng ta sẽ đầu tư tích hợp trực tiếp."

Kết quả: A ra mắt kịp, chiếm thị phần, và 8 tháng sau khi chạm ngưỡng, họ có đủ dữ liệu lẫn nguồn lực để trả nợ đúng lúc. Bài học: nợ cố ý-thận trọng có giá trị khi bạn định lượng được lãi và có điều kiện kích hoạt rõ ràng để trả.

Tình huống 2: Sàn thương mại điện tử và module Order "thần thánh"

Một công ty thương mại điện tử tầm trung (công ty B, khoảng 40 kỹ sư) khởi đầu với một module Order xử lý mọi thứ: giỏ hàng, tính giá, khuyến mãi, thanh toán, xuất kho. Ở giai đoạn đầu điều này hoàn toàn hợp lý — đây là nợ vô tình, không ai sai.

Nhưng khi công ty mở rộng sang B2B, thêm hình thức trả góp, tích hợp nhiều đơn vị vận chuyển, module Order phình lên hơn 12.000 dòng. Mỗi thay đổi nhỏ về khuyến mãi lại vô tình làm hỏng logic vận chuyển. Thời gian cho một feature trung bình tăng từ 3 ngày lên 9 ngày. Đây chính là "lãi kép" của tech debt.

Sai lầm của B: Tech Lead ban đầu đòi refactor toàn bộ trong một quý, dừng gần hết feature. Business từ chối vì không thấy giá trị kinh doanh. Cách làm đúng, mà cuối cùng họ áp dụng, là chiến lược "bóp nghẹt" (Strangler Fig pattern): mỗi khi chạm vào một phần của Order, họ tách phần đó ra thành module riêng có ranh giới rõ ràng. Sau 4 tháng, logic thanh toán và khuyến mãi đã được tách ra, thời gian feature giảm về 4 ngày — mà không cần một dự án refactor lớn nào. Bài học: nợ kiến trúc thường được trả tốt nhất theo kiểu tăng dần, gắn với công việc đang làm, chứ không phải một "big bang refactor".

Tình huống 3: Quyết định NoSQL vội vàng — nợ một chiều

Một công ty logistics ở Đông Nam Á (công ty C) chọn một database NoSQL cho hệ thống theo dõi đơn hàng vì "nó hot và scale tốt". Đây là quyết định cố ý nhưng liều lĩnh — không ai phân tích kỹ mô hình truy vấn.

Vấn đề lộ ra khi business cần các báo cáo phức tạp với nhiều điều kiện join và tổng hợp — điều mà NoSQL họ chọn làm rất tệ. Vì dữ liệu đã nằm sâu trong hệ thống, việc chuyển sang SQL là một quyết định một chiều cực đắt, tốn gần 6 tháng của 4 kỹ sư. Bài học: với các quyết định kiến trúc khó đảo ngược, sự "liều lĩnh" phải trả giá đắt nhất. Trước một quyết định một chiều, việc bỏ ra vài ngày làm một tài liệu đánh giá (ADR — Architecture Decision Record) rẻ hơn rất nhiều so với 6 tháng sửa sai.

Hướng dẫn từng bước

Đây là quy trình tôi khuyên bạn áp dụng để quản lý tech debt và quyết định kiến trúc một cách có hệ thống:

Bước 1 — Làm cho nợ trở nên hữu hình. Nợ vô hình thì không ai quản lý được. Tạo một "sổ nợ kỹ thuật" (tech debt register/backlog) — có thể chỉ là một danh sách trong Jira hoặc Notion. Mỗi mục ghi: mô tả, loại (cố ý/vô tình), tác động ước tính, và chi phí trả.

Bước 2 — Phân loại theo ma trận Fowler. Với mỗi khoản nợ, xác định nó thuộc góc phần tư nào. Điều này quyết định cách xử lý: nợ liều lĩnh cần fix quy trình/con người; nợ thận trọng cần lên lịch trả.

Bước 3 — Định lượng "lãi suất". Đừng nói "code này tệ". Hãy nói "vì cấu trúc này, mỗi tính năng liên quan đến thanh toán tốn thêm khoảng 3 ngày, và trong quý tới chúng ta có 5 tính năng như vậy — tức 15 ngày công bị đốt cho lãi." Con số nói chuyện được với business, cảm tính thì không.

Bước 4 — Với quyết định kiến trúc, viết ADR. Trước mỗi quyết định lớn, ghi một tài liệu ngắn (1 trang): bối cảnh, các phương án, quyết định chọn, hệ quả và loại nợ chấp nhận. Phân biệt rõ đây là quyết định một chiều hay hai chiều.

Bước 5 — Phân bổ ngân sách trả nợ cố định. Cam kết một tỷ lệ năng lực đội cho việc trả nợ — phổ biến là 15–20% mỗi sprint. Đây không phải "khi rảnh thì làm", mà là một khoản đầu tư thường xuyên, giống như bảo trì máy móc.

Bước 6 — Ưu tiên trả nợ theo "lãi", không theo "độ khó chịu". Trả nợ ở những vùng code thường xuyên bị chạm vào trước. Một đoạn code xấu nhưng không ai đụng đến trong 2 năm thì lãi bằng không — đừng phí công. Dùng dữ liệu (ví dụ số lần commit vào một file) để tìm điểm nóng.

Lỗi thường gặp & mẹo

Lỗi 1 — Coi mọi tech debt là như nhau. Không phải. Code xấu ở vùng ít đụng tới không đáng lo; coupling ở lõi hệ thống mới là nguy hiểm. Hãy phân biệt theo tần suất thay đổi.

Lỗi 2 — Đòi "big bang refactor". Dừng feature vài tháng để viết lại là cách nhanh nhất để mất niềm tin của business và tạo ra rủi ro khổng lồ (bạn có thể tạo ra bug mới nhiều hơn giá trị thu về). Ưu tiên trả nợ tăng dần theo Strangler Fig.

Lỗi 3 — Nói chuyện tech debt bằng ngôn ngữ kỹ thuật với người không kỹ thuật. "Chúng ta cần refactor vì coupling quá cao" chẳng có nghĩa gì với CEO. Hãy dịch sang: "Vì cấu trúc hiện tại, tốc độ ra tính năng đang chậm dần 30% mỗi quý, và rủi ro sự cố khi vào mùa cao điểm đang tăng."

Lỗi 4 — Vay nợ mà không ghi lại. Nợ cố ý mà không được ghi vào backlog thì sẽ biến thành nợ vô tình bị quên lãng — thứ tệ nhất. Luôn để lại dấu vết: một // TODO kèm ticket, một dòng trong ADR.

Mẹo: Áp dụng "Quy tắc hướng đạo sinh" (Boy Scout Rule) — mỗi lần chạm vào một file, hãy để nó sạch hơn một chút so với khi bạn tới. Đây là cách trả nợ rẻ nhất, không cần dự án riêng.

Mẹo: Khi đứng trước quyết định kiến trúc, hãy tự hỏi: "Đây là cửa một chiều hay hai chiều?" Nếu hai chiều, quyết nhanh và thử. Nếu một chiều, chậm lại và viết ADR.

Bài tập thực hành

  • Lập sổ nợ. Chọn một hệ thống bạn đang làm. Liệt kê 5 khoản tech debt lớn nhất. Với mỗi khoản, phân loại theo ma trận Fowler (cố ý/vô tình × thận trọng/liều lĩnh).
  • Định lượng lãi. Chọn 1 trong 5 khoản trên và ước tính "lãi suất" của nó bằng con số cụ thể: mỗi tính năng liên quan tốn thêm bao nhiêu ngày công? Trong quý tới có bao nhiêu tính năng như vậy?
  • Viết một ADR. Chọn một quyết định kiến trúc gần đây (hoặc sắp tới) trong đội bạn. Viết một tài liệu ADR 1 trang: bối cảnh, các phương án, quyết định, hệ quả, và loại nợ chấp nhận. Ghi rõ đây là quyết định một chiều hay hai chiều.
  • Dịch sang ngôn ngữ business. Lấy khoản nợ ở bài 2, viết một đoạn 3 câu giải thích cho một Product Manager không kỹ thuật vì sao nên đầu tư trả khoản nợ này — chỉ dùng ngôn ngữ tác động kinh doanh (tốc độ, rủi ro, chi phí).

Tóm tắt

  • Tech debt là một phép ẩn dụ tài chính: vay nợ không xấu, vay mù quáng mới xấu. Điều quan trọng là biết mình nợ bao nhiêu và có kế hoạch trả.
  • Ma trận Fowler (cố ý/vô tình × thận trọng/liều lĩnh) giúp bạn phân loại nợ và chọn hành động đúng. Nợ liều lĩnh là dấu hiệu vấn đề về quy trình hoặc năng lực, không chỉ là code xấu.
  • Nợ nghiêm trọng nhất đến từ quyết định kiến trúc. Phân biệt quyết định một chiều (chậm lại, viết ADR) và hai chiều (quyết nhanh, thử).
  • Trả nợ tốt nhất là tăng dần và có ưu tiên — theo điểm nóng thường bị chạm vào, gắn với công việc đang làm — chứ không phải "big bang refactor".
  • Luôn định lượng lãi bằng con số và dịch sang ngôn ngữ kinh doanh. Đó là kỹ năng phân biệt một Tech Lead giỏi với một kỹ sư giỏi.