Technical Roadmap, Nợ kỹ thuật & Rủi roTechnical Roadmap, Debt & Risk
Thuộc bộ kiến thức Engineering Manager Roadmap.
Tổng quan
Đằng sau bất kỳ product roadmap nào cũng có một technical roadmap ẩn bên dưới, dù có ai viết nó ra hay không. Feature nào cũng cần một nền tảng (platform) để chạy trên đó, nền tảng đó tích lũy dần các đường tắt (shortcut) và “sẹo” theo thời gian, và mọi sáng kiến mới đều mang một xác suất nào đó sẽ đi chệch hướng. Công việc của một engineering manager (EM) là làm cho lớp ẩn này trở nên rõ ràng: lên kế hoạch đầu tư platform để feature work trong tương lai còn khả thi, giữ một bản ghi trung thực, cập nhật liên tục về những shortcut mà team đã chấp nhận, và đánh giá rủi ro của một hạng mục công việc trước khi team cam kết một deadline dựng trên nền đó.
Không có gì kỳ lạ ở đây cả. Đây chỉ là cùng một kỷ luật áp dụng cho ba đối tượng khác nhau — tương lai (roadmap), quá khứ (debt), và điều chưa biết (risk) — và ba thứ này gắn chặt với nhau. Một roadmap bỏ qua debt sẽ tiếp tục phải trả cùng một khoản “lãi” trên mỗi feature mới. Một roadmap bỏ qua risk thỉnh thoảng sẽ “nổ” công khai, đúng ngày, trước mặt khách hàng. Và debt với risk cũng liên quan đến nhau: debt không được ghi chép là một yếu tố nhân rủi ro, vì không ai có thể đánh giá một rủi ro mà họ không biết là nó tồn tại.
Note này bàn về việc xây dựng và truyền đạt một technical roadmap, hiểu và quản lý technical debt mà không coi nó là một sai lầm đạo đức, thực hiện risk assessment nhẹ nhàng trước khi cam kết công việc, quyết định build vs. buy, và việc retire (khai tử) legacy system cũng như adopt công nghệ mới mà không lao từ thái cực này sang thái cực kia. Note này dựa trên ./09-technical-decision-making-and-architecture.md — nơi các quyết định architecture hiện thực hóa roadmap, debt, và risk — và dẫn tới ./15-strategy-business-case-and-prioritization.md, nơi các cân nhắc kỹ thuật này cạnh tranh cùng một chỗ ưu tiên với feature work.
Kiến thức nền tảng
Technical roadmap để làm gì
Technical roadmap là một kế hoạch có thứ tự (sequenced) về platform, infrastructure, và architecture work cần thiết để tiếp tục giao (deliver) product roadmap với một mức chi phí và rủi ro chấp nhận được — nó không phải là một product roadmap thứ hai, và cũng không phải danh sách những thứ engineer thấy thú vị. Phép thử để biết một hạng mục có thuộc technical roadmap hay không là: nó có thay đổi những gì team có thể deliver, nhanh đến đâu, an toàn đến đâu, hay tốn kém đến đâu hay không. Một migration gỡ bỏ một trần scale (scaling ceiling) trước khi product cần đến mức scale đó thì thuộc về roadmap. Một cuộc rewrite làm code “đẹp hơn” nhưng không thay đổi những gì team có thể ship thì không — dù rất hấp dẫn khi đóng khung nó theo cách đó.
Một mô hình tư duy hữu ích: product roadmap mô tả giá trị gì sẽ được ship, còn technical roadmap mô tả hệ thống cần đúng những gì để giá trị đó ship đúng lịch và vận hành rẻ. Technical roadmap tốt vì vậy nên được suy ra (derived), chứ không phải nghĩ ra một cách độc lập: với mỗi theme lớn trên product roadmap trong hai đến bốn quý tới, hãy hỏi hệ thống cần làm được gì, rồi lùi ngược lại các khoản đầu tư platform để hiện thực hóa điều đó. Nếu product roadmap có kế hoạch mở rộng sang ba khu vực mới, technical roadmap cần có công việc về data residency và latency đa vùng từ rất lâu trước quý đó, chứ không phải trong lúc đó.
Sắp thứ tự platform investment và feature work
Căng thẳng muôn thuở là platform work hiếm khi có một kết quả kinh doanh riêng, có ngày tháng rõ ràng — nó cho phép các kết quả xảy ra chứ bản thân nó không phải là một kết quả — trong khi feature work gần như luôn có. Nếu để cạnh tranh chỉ dựa trên độ “hiện diện”, platform work sẽ luôn thua, cho tới khi hệ thống không thể chịu thêm một feature nào nữa mà không gãy, lúc đó nó mới được cấp ngân sách trong hoảng loạn, với chi phí tệ hơn nhiều so với nếu đã lên kế hoạch từ trước. Việc của EM là ngăn chặn cơn hoảng loạn đó bằng cách biến platform investment thành một mục có lịch trình, hiển thị rõ ràng, thay vì một cuộc vật lộn phản ứng (reactive).
Một vài nguyên tắc sắp thứ tự chứng tỏ hiệu quả trong thực tế:
- Làm sớm những việc nền tảng sẽ đắt hơn theo thời gian. Thay đổi data model rẻ khi bảng có một nghìn dòng và đau đớn khi nó có một tỷ dòng. Nếu một quyết định nền tảng chắc chắn sẽ phải xem lại, hãy xem lại khi nó còn rẻ.
- Sắp platform work ngay trước feature work mà nó mở đường, không quá sớm để nó nằm im không dùng và trôi khỏi thực tế sử dụng, cũng không quá muộn để feature bị kẹt chờ. Điều này đòi hỏi EM thực sự hiểu hình dạng của product roadmap, không chỉ sprint kế tiếp.
- Gộp platform work với feature work liền kề khi có thể, để một team đụng vào một subsystem vì một feature cũng trả bớt debt hoặc củng cố subsystem đó trong cùng một lượt — rẻ hơn nhiều so với một dự án riêng sau này, và dễ xin ngân sách hơn nhiều vì nó đi kèm với thứ có kết quả hiển thị.
- Bảo vệ một mức sàn năng lực (capacity floor) tối thiểu cho platform work ngay cả khi product roadmap đang bị áp lực. Nếu platform investment chỉ xảy ra trong thời gian dư ra sau feature, nó sẽ không bao giờ xảy ra, vì một tổ chức khỏe mạnh, tham vọng không bao giờ có thời gian dư. Xem phần phân bổ capacity bên dưới để cụ thể hóa điều này.
Truyền đạt technical roadmap tới stakeholder phi kỹ thuật
Một technical roadmap mà chỉ người kỹ thuật hiểu là một roadmap sẽ thua mọi cuộc tranh giành ưu tiên mà nó tham gia, vì những người ra quyết định đánh đổi ở cấp trên EM — product leader, tài chính, đôi khi cả ban điều hành — không thể cấp ngân sách cho thứ họ không hiểu giá trị. Lỗi truyền đạt lớn nhất là trình bày platform work bằng ngôn ngữ triển khai (“migrate khỏi monolith”, “nâng cấp message queue”) thay vì ngôn ngữ kết quả (“giảm một nửa tỷ lệ incident ở checkout”, “mở đường để launch tại EU mà không cần lead time sáu tháng”). Technical roadmap cần một lớp phiên dịch, mỗi khi nó rời khỏi phòng họp của engineer.
Ba thói quen giúp lớp phiên dịch này bám chắc. Thứ nhất, gắn một hệ quả kinh doanh vào mỗi hạng mục roadmap — không phải mô tả kỹ thuật, mà một câu về điều gì sẽ gãy, chậm lại, hoặc trở nên bất khả thi nếu công việc không được làm, đóng khung bằng những thứ stakeholder đã quan tâm sẵn (chi phí, tốc độ, rủi ro, độ tin cậy nhìn thấy được với khách hàng). Thứ hai, dùng những phép so sánh (analogy) mà stakeholder đã tin tưởng sẵn — so sánh việc trì hoãn infrastructure work với việc trì hoãn bảo trì một tòa nhà hay một chiếc xe (nó không khiến tòa nhà ngừng dùng được ngay, nhưng cái giá của việc bỏ qua tích lũy dần và cuối cùng trở thành một tình huống khẩn cấp) thường “vào” tốt hơn bất kỳ sơ đồ architecture nào. Thứ ba, giữ một artifact roadmap hiển thị, có ngày tháng — dù chỉ là một slide với các swimlane cho “feature”, “platform”, và “giảm rủi ro” — để platform work được nhìn thấy nằm trong cùng một kế hoạch với mọi thứ khác, chứ không nằm riêng trong một tài liệu dễ bị hạ ưu tiên vì dễ bị quên. Xem ./13-stakeholder-communication-and-reporting.md để biết bộ kỹ thuật rộng hơn cho việc điều chỉnh giao tiếp kỹ thuật theo từng đối tượng.
Ẩn dụ technical debt, hiểu cho đúng
Ward Cunningham đặt ra thuật ngữ “technical debt” năm 1992 để giải thích, cho một đối tượng kinh doanh, tại sao ship code nhanh mà không tinh chỉnh (refine) thiết kế nền tảng tạo ra một nghĩa vụ (obligation) phải trả sau này, theo cách này hay cách khác — không phải để cho engineer cái cớ viết code tệ rồi gọi đó là chiến lược. Chính lời giải thích của ông, đưa ra nhiều năm sau vì ẩn dụ đã trôi khỏi ý định ban đầu, nói rõ rằng bản thân debt không phải là vấn đề: vay debt để ship sớm hơn có thể là một quyết định hoàn toàn hợp lý, miễn là nó được trả nhanh chóng, giống như một khoản vay kinh doanh có thể tài trợ cho tăng trưởng miễn là được trả đều. Điều Cunningham phản đối là code không bao giờ được refactor theo những gì thực sự học được trong lúc xây dựng nó — debt được vay rồi không bao giờ trả, âm thầm tích lũy.
Martin Fowler sau đó tinh chỉnh điều này thành Technical Debt Quadrant, tách debt theo hai trục độc lập: nó có phải là có chủ đích hay vô tình (deliberate/inadvertent), và nó có thận trọng hay liều lĩnh (prudent/reckless). Sự kết hợp này quan trọng hơn nhiều so với việc chỉ đơn thuần biết debt tồn tại, vì nó thay đổi phản ứng đúng đắn nên là gì.
| Liều lĩnh (Reckless) | Thận trọng (Prudent) | |
|---|---|---|
| Có chủ đích (Deliberate) | “Chúng ta không có thời gian để thiết kế”, được coi như một cái cớ thường xuyên hơn là một sự đánh đổi thực sự | ”Chúng ta phải ship ngay bây giờ và xử lý hậu quả sau” — một đánh đổi có ý thức, có thông tin để đạt một deadline thực sự |
| Vô tình (Inadvertent) | “Layering là gì?” — debt phát sinh chỉ vì chưa biết cách làm tốt hơn | ”Giờ chúng ta đã biết lẽ ra nên làm thế nào” — debt chỉ trở nên rõ ràng khi nhìn lại, sau khi team đã hiểu vấn đề sâu hơn |
Đọc xuyên suốt quadrant này, ý nghĩa thực tiễn là debt không tự thân là xấu, và đôi khi lại là quyết định kỹ thuật đúng đắn. Một team có chủ đích và thận trọng ship một implementation đơn giản hóa để kịp một deadline thị trường hay hợp đồng có thật, hoàn toàn ý thức được điều gì đã bị bỏ qua và có kế hoạch quay lại xem xét, đã đưa ra một quyết định tốt — phương án thay thế (trễ deadline để xây phiên bản lý tưởng) rất có thể còn tệ hơn cho doanh nghiệp. Ngược lại, debt liều lĩnh — dù có chủ đích (“chúng ta không bao giờ có thời gian cho việc này”) hay vô tình (đơn giản là chưa biết cách làm tốt hơn) — mới là loại bào mòn codebase và khả năng vận động của team, vì nó hoàn toàn không phải là một sự đánh đổi có cân nhắc.
Tại sao vấn đề thật sự là debt không được theo dõi
Nếu debt có chủ đích, thận trọng có thể là quyết định đúng, thì nguy hiểm thật sự không phải là debt — mà là debt không ai còn nhớ đã vay. Nợ tài chính có một bên cho vay gửi sao kê mỗi tháng; technical debt không có cơ chế cưỡng chế như vậy trừ khi team tự xây dựng nó. Bị bỏ mặc không theo dõi, debt không tự thông báo sự tồn tại của mình: nó chỉ âm thầm làm mỗi thay đổi tiếp theo chậm hơn và rủi ro hơn một chút, theo cách gần như không thể quy về một quyết định cụ thể nào, cho tới khi team phải trả một khoản “thuế debt” nặng nề trên mỗi đơn vị feature work mà không thể nói rõ tại sao.
Đây chính là điểm mấu chốt của cả chủ đề, và nó định hình lại công việc của người quản lý — từ “xóa bỏ technical debt” (một mục tiêu bất khả thi và thậm chí không đáng mong muốn) thành “đảm bảo mọi khoản debt là một quyết định đã biết, có chủ đích, được theo dõi, có chủ sở hữu và một kế hoạch sơ bộ” — điều này khả thi, và là trọng tâm của phần còn lại của note này.
Khái niệm chính
Làm cho technical debt hiển thị và có thể ưu tiên hóa
Debt chỉ sống trong đầu engineer thì không cạnh tranh với gì cả, vì nó không bao giờ bước vào bất kỳ cuộc trò chuyện ưu tiên hóa nào — nó chỉ âm thầm bị phát hiện lại, và bị chịu đựng lại, mỗi khi có ai đó đụng vào phần code đó. Cách khắc phục mang tính cơ học: cho debt cùng loại hạ tầng hiển thị mà feature đã có.
Debt register là phiên bản đơn giản nhất của điều này: một danh sách sống (một label trong backlog, một board riêng, hoặc thậm chí một tài liệu chia sẻ, tùy quy mô team) nơi mỗi mục ghi lại shortcut là gì, tại sao nó được chọn, chi phí để sửa bây giờ so với sau này, chi phí hiện tại mà nó đang gây ra cho team (giao hàng chậm hơn ở một khu vực cụ thể, một loại bug tái diễn, gánh nặng on-call), và ai chịu trách nhiệm theo dõi. Register quan trọng ít hơn ở công cụ cụ thể mà quan trọng hơn ở kỷ luật: không có gì được coi là “chúng ta đã đi tắt” mà không đồng thời đi vào register, như một chuẩn mực của team, không phải sự cưỡng chế từ manager.
Gắn tag debt trực tiếp trong backlog — một label tech-debt, một epic debt riêng, hoặc một tag mức độ nghiêm trọng nhẹ nhàng — giữ nó trong cùng công cụ lập kế hoạch và ưu tiên hóa mà team đã dùng, thay vì trong một hệ thống riêng dễ bị quên là tồn tại. Điều này cũng khiến debt có thể báo cáo được: một manager có thể xem “chúng ta có bao nhiêu debt được gắn tag, và mục cũ nhất đã bao lâu rồi” như một con số thật, thay vì một cảm giác mơ hồ, luôn luôn đúng rằng “chúng ta thật sự nên dọn dẹp cái này lúc nào đó”.
Tuy nhiên, bước tạo ra khác biệt thực tiễn lớn nhất là phân bổ một tỷ lệ phần trăm capacity cố định cho việc trả debt — thường ở đâu đó trong khoảng 10-20%, dù con số đúng phụ thuộc vào tuổi đời hệ thống và lượng debt đã tích lũy — thay vì trông chờ vào “chúng ta sẽ làm khi mọi thứ lắng xuống”. Mọi thứ không bao giờ lắng xuống ở một sản phẩm khỏe mạnh, đang tăng trưởng; trông chờ vào thời gian rảnh để trả debt nghĩa là trả debt phải cạnh tranh với feature work theo luật chơi của feature work, và luôn thua, vì feature work luôn có một deadline gần hơn, hiển thị hơn. Một tỷ lệ được bảo vệ biến việc trả debt từ một hy vọng thành một cam kết thường trực sống sót được khi va chạm với một roadmap bận rộn, giống cách một khoản tiết kiệm cố định bị trừ tự động sống sót được khi va chạm với một món hàng hấp dẫn, theo cách mà “tôi sẽ tiết kiệm phần còn dư” không bao giờ làm được.
| Cách tiếp cận | Nó làm gì | Chế độ thất bại nếu bỏ qua |
|---|---|---|
| Debt register | Ghi lại điều gì bị bỏ qua, tại sao, và chi phí của nó | Debt bị phát hiện lại một cách đau đớn, không có ký ức tổ chức về lý do nó tồn tại |
| Gắn tag trong backlog | Làm debt hiển thị trong cùng công cụ lập kế hoạch với feature | Debt sống trong một tài liệu riêng, dễ bị bỏ qua, hoặc trong đầu không ai cả |
| Phân bổ capacity cố định | Đảm bảo việc trả debt xảy ra theo nhịp | Trả debt chỉ xảy ra trong “thời gian rảnh”, vốn không bao giờ đến một cách đáng tin cậy |
| Nhịp review debt | Định kỳ ưu tiên hóa lại register khi bối cảnh thay đổi | Các mục debt cũ, không còn liên quan nằm lại và chiếm chỗ những mục hiện đang quan trọng |
Đánh giá rủi ro kỹ thuật (technical risk assessment)
Debt là rủi ro đã xảy ra rồi; risk assessment là về debt — hay tệ hơn, về sự cố (outage), deadline lỡ hẹn, migration thất bại — chưa xảy ra nhưng có thể xảy ra. Trước khi giao một dự án cho team, đáng để chủ động hỏi mọi thứ có thể sai ở đâu, thay vì phát hiện ra giữa dự án khi chi phí phát hiện là cao nhất. Các nguồn rủi ro kỹ thuật phổ biến, đáng kiểm tra cho mọi dự án không tầm thường, gồm:
- Công nghệ không quen thuộc — team đang dùng một ngôn ngữ, framework, hay platform chưa từng ship cùng trước đây, nên ước lượng dựa trên đoán mò thay vì kinh nghiệm.
- Deadline gấp hoặc bị cố định từ bên ngoài — một deadline do hợp đồng, quy định pháp lý, hoặc cam kết marketing thúc đẩy, loại bỏ khả năng đơn giản là kéo dài lịch trình nếu có sự cố.
- Phụ thuộc vào một cá nhân chủ chốt (key-person dependency) — kiến thức quan trọng tập trung vào một người, nên việc họ không sẵn sàng (ốm, nghỉ việc, thậm chí nghỉ phép) làm trì trệ toàn bộ nỗ lực.
- Độ tin cậy của bên thứ ba và vendor — dự án phụ thuộc vào một API, dịch vụ, hoặc vendor bên ngoài mà uptime, roadmap, hay sự tồn tại liên tục của họ team không kiểm soát được.
- Yêu cầu không rõ ràng hoặc thay đổi liên tục — team được yêu cầu xây dựng theo một đặc tả (specification) nhiều khả năng sẽ thay đổi, có nguy cơ phải bỏ đi công sức đã làm.
- Độ phức tạp tích hợp — công việc đụng vào nhiều hệ thống hiện có, mỗi hệ thống là một cơ hội cho một tương tác không mong đợi.
Một cách đơn giản và hiệu quả để biến danh sách này thành một công cụ ra quyết định là dùng cùng khuôn mẫu likelihood × impact (khả năng xảy ra × mức độ ảnh hưởng) như trong đánh giá rủi ro bảo mật — xem ../devsecops/en/06-threat-modeling-and-risk-assessment.md để có phần trình bày đầy đủ hơn về khuôn mẫu này áp dụng cho security threat cụ thể. Với mỗi rủi ro đã xác định, chấm điểm likelihood (khả năng nó thực sự xảy ra với team) và impact (mức độ tệ nếu nó xảy ra) trên một thang đơn giản, thường 1-5, rồi nhân chúng lại để có một điểm ưu tiên sơ bộ.
| Rủi ro | Likelihood (1-5) | Impact (1-5) | Điểm | Giảm thiểu |
|---|---|---|---|---|
| Framework không quen thuộc với cả team | 4 | 3 | 12 | Spike hoặc pilot có giới hạn thời gian trước khi giao cả team; mời chuyên gia bên ngoài review |
| Deadline cứng từ bên ngoài (quy định pháp lý) | 2 | 5 | 10 | Xây dựng buffer; xác định sớm một phương án phạm vi thu hẹp dự phòng |
| Chỉ một engineer hiểu hệ thống billing legacy | 4 | 4 | 16 | Pair on việc này ngay bây giờ; ghi chép khi đụng vào; không để tình trạng này kéo dài thêm một chu kỳ nữa |
| Lịch sử outage của vendor thanh toán bên thứ ba | 2 | 4 | 8 | Thêm một vendor dự phòng hoặc một đường graceful-degradation |
Điểm số không phải là một phép đo chính xác — nó là một cơ chế bắt buộc (forcing function) khiến các rủi ro được gọi tên và so sánh trên cùng một mặt bằng, thay vì để lại như một sự lo lắng mơ hồ không bao giờ thực sự bước vào cuộc trò chuyện lập kế hoạch. Các mục điểm cao xứng đáng có một biện pháp giảm thiểu rõ ràng trước khi dự án được cam kết một ngày cụ thể; các mục điểm thấp có thể được chấp nhận và chỉ cần theo dõi. Kết quả của bài tập này nên đi thẳng vào cuộc trò chuyện về lịch trình và nhân sự, chứ không nằm trong một tài liệu không ai xem lại — một dự án với rủi ro key-person điểm 16 không nên được lên lịch giống như một dự án không có rủi ro đó.
Đánh giá build vs. buy
Mọi năng lực không tầm thường đều đặt ra câu hỏi nên tự xây dựng (build) trong nội bộ hay mua/tích hợp từ bên ngoài (buy — như một sản phẩm vendor, một managed service, hay một open-source dependency), và làm sai theo bất kỳ hướng nào cũng tốn kém: xây dựng infrastructure không khác biệt mà một vendor đã làm tốt sẵn lãng phí thời gian engineering mà doanh nghiệp cần cho việc khác, trong khi mua một thứ hóa ra là điểm khác biệt cạnh tranh cốt lõi lại khóa lợi thế cạnh tranh của công ty vào roadmap của người khác.
Lăng kính hữu ích nhất là sự phân biệt, được Amazon phổ biến trong bối cảnh này, giữa core competency (năng lực cốt lõi) và undifferentiated heavy lifting (công việc nặng nhọc không tạo khác biệt). Nếu một năng lực là một phần của điều làm sản phẩm thực sự khác biệt với khách hàng — thứ mà khách hàng sẽ nhận ra và quan tâm nếu nó tệ hơn — đó là một ứng viên mạnh để build và sở hữu, vì sở hữu nó chính là sở hữu sự khác biệt. Nếu một năng lực là cần thiết nhưng vô hình với khách hàng và không phải nguồn lợi thế cạnh tranh — authentication, xử lý thanh toán, gửi email, container orchestration — nó thường là undifferentiated heavy lifting, và tự xây dựng nghĩa là tiêu tốn thời gian engineering quý giá để tái phát minh thứ mà một vendor chuyên biệt đã dành nhiều năm để hoàn thiện.
Ngoài lăng kính cốt lõi đó, một vài yếu tố khác luôn nên có mặt trong quyết định:
| Yếu tố | Nghiêng về build | Nghiêng về buy |
|---|---|---|
| Khác biệt hóa chiến lược | Năng lực là cốt lõi của lợi thế cạnh tranh | Năng lực cần thiết nhưng vô hình với khách hàng |
| Total cost of ownership (TCO) | Giá vendor scale tệ hơn chi phí nội bộ ở khối lượng của công ty | Chi phí nội bộ (thời gian engineering, bảo trì liên tục, gánh nặng on-call) vượt giá vendor khi tính đủ |
| Thời gian ra thị trường (time to market) | Team có đúng chuyên môn và thời gian cần thiết, và tốc độ tự xây nhanh hơn tốc độ tích hợp | Cần một giải pháp hoạt động sớm hơn khả năng thực tế của một cuộc build nội bộ |
| Nhu cầu tùy biến | Yêu cầu đủ đặc thù để không sản phẩm vendor nào vừa vặn mà không cần workaround nặng | Chức năng tiêu chuẩn đáp ứng đủ nhu cầu; edge case đủ hiếm để chấp nhận sống chung |
| Rủi ro vendor lock-in | Chi phí chuyển đổi hoặc rủi ro phụ thuộc vendor không chấp nhận được với một năng lực trọng yếu như vậy | Rủi ro lock-in chấp nhận được với sự ổn định của vendor, hoặc được giảm thiểu bằng một lớp abstraction / chuẩn mở |
| Gánh nặng bảo trì liên tục | Team muốn sở hữu việc bảo trì và phát triển năng lực này lâu dài | Team không muốn gánh nặng bảo trì vĩnh viễn cho thứ nằm ngoài trọng tâm cốt lõi |
| Bảo mật & tuân thủ | Kiểm soát nội bộ được yêu cầu bởi quy định hoặc hợp đồng | Vendor đã có sẵn các chứng nhận liên quan mà team sẽ mất nhiều năm để đạt được |
Total cost of ownership đáng được nhấn mạnh đặc biệt vì đây là yếu tố thường bị ước lượng sai nhất, gần như luôn theo hướng đánh giá thấp chi phí build. Chi phí “trên tiêu đề” của một cuộc build nội bộ là thời gian engineering ban đầu, nhưng chi phí thật sự của nó còn bao gồm mọi lần sửa bug tiếp theo, vá bảo mật, page on-call, nâng cấp dependency, và chi phí cơ hội của những engineer lẽ ra có thể xây dựng sản phẩm — những chi phí mà vendor đã khấu hao trên nhiều khách hàng rồi. Một kỷ luật hữu ích là định giá một quyết định build không phải ở chi phí ban đầu, mà ở tổng chi phí nhiều năm, bao gồm một tỷ lệ nhân sự bảo trì thực tế, trước khi so sánh với báo giá vendor.
Khai tử (retire) legacy system
Legacy system tồn tại lâu dài vì những lý do hợp lý ở cấp độ quyết định cá nhân, ngay cả khi kết quả tổng thể rõ ràng là xấu cho tổ chức. Hai lực chi phối chuyện này. Thứ nhất là nỗi sợ: một hệ thống không ai còn hiểu đầy đủ nữa, được xây bởi những người đã rời đi, thật đáng sợ để đụng vào, vì bất kỳ thay đổi nào cũng có thể làm gãy thứ gì đó mà mục đích không được ghi chép và chế độ hỏng (failure mode) không ai biết — nên con đường ít kháng cự nhất luôn là xây thêm một workaround nữa lên trên, thay vì mạo hiểm khai tử. Thứ hai là sunk cost (chi phí chìm): một hệ thống mất nhiều năm và ngân sách đáng kể để xây cảm giác lãng phí khi khai tử, ngay cả khi so sánh trung thực hoàn toàn hướng về tương lai (chi phí để tiếp tục chạy so với chi phí để thay thế), vì khoản đầu tư đã chìm mang tính cảm xúc, nếu không muốn nói là kinh tế, đối với những người đã xây nó.
Liều thuốc thực tiễn cho cả hai lực này là khai tử hệ thống theo từng bước tăng dần thay vì một lần duy nhất, để không bước nào đòi hỏi phải hiểu đầy đủ — hay tin tưởng đầy đủ — toàn bộ legacy system trước khi bắt đầu. Strangler fig pattern (đặt theo tên loài dây leo mọc quanh cây chủ, dần dần thay thế vai trò cấu trúc của nó cho tới khi cây gốc có thể bị loại bỏ) là cách tiếp cận chuẩn: chức năng mới được xây dựng quanh rìa legacy system, traffic đến dần dần được chuyển hướng từ năng lực cũ sang mới từng mảnh một, và dấu chân (footprint) của legacy system co lại liên tục thay vì một lần, cho tới khi phần còn lại có thể tắt an toàn. Xem ../backend/en/14-software-architecture.md để có phần trình bày architecture đầy đủ hơn về pattern này và cách nó so với một cuộc rewrite “big-bang”. Strangler fig pattern là điều khiến việc khai tử khả thi về mặt chính trị lẫn vận hành: nó không bao giờ đòi hỏi một ngày cutover lớn, rủi ro cao; nó tạo ra tiến độ liên tục, dễ chứng minh, dễ báo cáo; và nó cho phép team rút lui khỏi bất kỳ bước nào mà không đốt cháy toàn bộ cuộc migration.
Khi traffic của một thành phần legacy đã thực sự được migrate hết, việc decommission (ngừng hoạt động) nó là một dự án riêng với rủi ro thất bại âm thầm của chính nó, và cần một checklist thay vì giả định rằng “không ai dùng nữa, cứ tắt thôi”:
- Xác nhận không còn traffic/usage trong một khoảng thời gian đại diện (không chỉ một ngày thứ Ba vắng vẻ) bằng monitoring thật, không phải trí nhớ của developer.
- Xác định và migrate hoặc archive mọi dữ liệu vẫn chỉ tồn tại trong legacy system.
- Kiểm tra các bên phụ thuộc gián tiếp — job báo cáo, dịch vụ khác, batch job theo lịch, hay thậm chí code của team khác — gọi vào legacy system mà không hiển nhiên gắn với luồng traffic chính của nó.
- Tắt hệ thống theo cách có thể đảo ngược trước (ví dụ: vô hiệu hóa write, trả về lỗi, hoặc redirect ở tầng mạng) trước khi xóa vật lý infrastructure, và giữ trạng thái đó trong một khoảng thời gian quan sát đã thống nhất.
- Truyền đạt ngày ngừng hoạt động rộng rãi, kể cả với các team có thể có phụ thuộc không được ghi chép, từ rất lâu trước khi ngày đó đến.
- Chỉ gỡ bỏ infrastructure, credential, monitoring, và on-call rotation sau khi giai đoạn quan sát trôi qua suôn sẻ.
- Ghi chép lại những gì đã khai tử và tại sao, làm ký ức tổ chức cho lần sau ai đó hỏi “khoan, chúng ta từng có một hệ thống cho việc này mà?”
Chuyển đổi công cụ và tiếp nhận công nghệ mới
Team thất bại trong việc tiếp nhận công nghệ theo một trong hai cách đối lập nhau, và cả hai đều đủ phổ biến để gọi tên. Cách thứ nhất là đuổi theo mọi framework, database, hay công cụ mới ngay khi nó thành trend, làm phân mảnh chuyên môn của team trên quá nhiều công nghệ, khiến việc tuyển dụng và onboarding khó hơn, và thường đổi một tập vấn đề đã biết lấy một tập vấn đề chưa biết hóa ra còn tệ hơn. Cách thứ hai là từ chối hiện đại hóa hoàn toàn, cuối cùng khiến team mắc kẹt với công cụ không còn được hỗ trợ, không an toàn, hoặc đơn giản là quá lạc hậu so với thực hành hiện tại khiến việc tuyển dụng trở nên khó khăn vì ứng viên không muốn làm việc với nó.
Một khuôn khổ tiếp nhận nhẹ nhàng tránh cả hai thái cực bằng cách coi công nghệ mới như một giả thuyết cần được kiểm chứng rẻ tiền trước khi được tin tưởng rộng rãi:
- Xác định một vấn đề thật trước tiên, không phải một công nghệ đi tìm use case — động lực nên là “cách tiếp cận hiện tại của chúng ta đang gây ra nỗi đau X”, không phải “tôi đọc về công cụ này và nó trông ngầu”.
- Pilot ở quy mô nhỏ, trên một phần công việc thật, có ranh giới, ít rủi ro — không phải một dự án đồ chơi, vốn không thể phơi bày các vấn đề tích hợp hay vận hành thật, nhưng cũng không phải một hệ thống trọng yếu với doanh nghiệp, vốn khiến pilot quá rủi ro để trung thực về thất bại.
- Đặt tiêu chí thành công trước khi bắt đầu pilot, không phải sau — “điều này đã hoạt động” và “điều này đã không hoạt động” trông cụ thể như thế nào, để kết quả không bị đánh giá theo kết quả mà team đã đầu tư cảm xúc sẵn.
- Đo lường pilot một cách trung thực theo các tiêu chí đó, bao gồm cả những chi phí không xuất hiện trong một bản demo: gánh nặng vận hành, đường cong học tập cho phần còn lại của team, chất lượng tài liệu và hỗ trợ cộng đồng, và cách nó hành xử khi thất bại, không chỉ khi mọi thứ suôn sẻ.
- Quyết định rõ ràng để scale hoặc từ chối, và quan trọng là, thực sự từ chối một số pilot — một khuôn khổ mà mọi pilot đều âm thầm được lên production bất kể kết quả thì không đánh giá gì cả, đó chỉ là adoption với vài bước thừa.
- Nếu scale, lên kế hoạch migration một cách có chủ đích — bao gồm ai sở hữu giai đoạn chuyển tiếp khi cả công cụ cũ và mới cùng tồn tại, và một ngày mục tiêu để công cụ cũ được khai tử hoàn toàn, để “tiếp nhận” một thứ mới không đơn giản là mãi mãi hỗ trợ song song hai bộ công cụ.
Best Practices
Hãy coi những điều này là thói quen cần xây dựng vào nhịp thường xuyên của team, chứ không phải bài tập một lần, vì roadmap, debt, và risk đều xuống cấp nếu chỉ được review một lần:
- Duy trì một tài liệu technical roadmap sống, song hành với product roadmap, được review và sắp lại thứ tự theo cùng nhịp với product planning, không phải như một hoạt động phụ hàng năm.
- Cho mỗi mục debt một chủ sở hữu và một ước lượng chi phí sơ bộ — một debt register không có chủ sở hữu là một to-do list không ai chịu trách nhiệm.
- Bảo vệ một tỷ lệ phần trăm cố định của mỗi chu kỳ lập kế hoạch cho việc trả debt, và bảo vệ nó trong các cuộc trò chuyện lập kế hoạch giống như cách một cam kết deadline được bảo vệ.
- Chạy một lượt đánh giá rủi ro likelihood × impact nhẹ nhàng trước khi cam kết bất kỳ dự án nào có deadline bên ngoài, công nghệ không quen thuộc, hoặc một điểm lỗi duy nhất (single point of failure) về con người hay vendor.
- Mặc định buy cho năng lực không khác biệt và build cho khác biệt hóa cạnh tranh thực sự, và tranh luận lại một cách trung thực về total cost of ownership, bao gồm nhân sự bảo trì, trước khi giả định rằng build rẻ hơn.
- Khai tử legacy system theo từng bước tăng dần với cách tiếp cận strangler fig thay vì đặt cược mọi thứ vào một lần cutover duy nhất, và dùng một decommissioning checklist thay vì tin vào trí nhớ rằng một hệ thống không còn được dùng.
- Pilot công cụ mới trên công việc thật, có ranh giới, với tiêu chí thành công đặt trước, và sẵn sàng từ chối một pilot — một quy trình tiếp nhận công nghệ không bao giờ nói không thì thực sự không đánh giá gì cả.
- Báo cáo các hạng mục technical roadmap cho stakeholder phi kỹ thuật bằng ngôn ngữ kết quả, không phải ngôn ngữ triển khai, và giữ chúng hiển thị trong cùng một kế hoạch với feature work thay vì trong một tài liệu riêng dễ bị hạ ưu tiên.
Tài liệu tham khảo
- Ward Cunningham, “Ward Explains Debt Metaphor” — lời giải thích gốc năm 1992 về ẩn dụ technical debt, từ chính người đặt ra thuật ngữ này.
- Martin Fowler, “TechnicalDebt” — tổng quan về ẩn dụ và ý nghĩa thực tiễn của nó.
- Martin Fowler, “TechnicalDebtQuadrant” — khuôn khổ deliberate/inadvertent, prudent/reckless được nhắc ở trên.
- Martin Fowler, “StranglerFigApplication” — pattern thay thế legacy tăng dần.
./09-technical-decision-making-and-architecture.md— cách các quyết định architecture hiện thực hóa cụ thể sự đánh đổi giữa roadmap, debt, và risk.../devsecops/en/06-threat-modeling-and-risk-assessment.md— khuôn mẫu chấm điểm rủi ro likelihood × impact áp dụng cho security threat.../backend/en/14-software-architecture.md— phần trình bày architecture sâu hơn về strangler fig pattern và chiến lược migration../15-strategy-business-case-and-prioritization.md— cách các hạng mục technical roadmap cạnh tranh ưu tiên cùng feature work.
Part of the Engineering Manager Roadmap knowledge base.
Overview
Every product roadmap has a technical roadmap hiding underneath it, whether anyone has written it down or not. Features need a platform to run on, that platform accumulates shortcuts and scar tissue over time, and every new initiative carries some chance of going sideways. An engineering manager’s job is to make that hidden layer explicit: to plan the platform investments that make future feature work possible, to keep a running, honest account of the shortcuts the team has taken, and to size up the risk in a piece of work before the team is committed to a deadline built on top of it.
None of this is exotic. It is the same discipline applied to three different objects — the future (roadmap), the past (debt), and the unknown (risk) — and the three are tightly coupled. A roadmap that ignores debt will keep re-paying the same interest on every new feature. A roadmap that ignores risk will occasionally blow up in public, on a date, in front of a customer. And debt and risk are themselves related: undocumented debt is a risk multiplier, because nobody can assess a risk they don’t know exists.
This note covers building and communicating a technical roadmap, understanding and managing technical debt without treating it as a moral failing, doing lightweight risk assessment before committing to work, deciding when to build versus buy, and retiring legacy systems and adopting new tools without lurching between extremes. It builds on ./09-technical-decision-making-and-architecture.md — architecture decisions are where roadmap, debt, and risk all get made concrete — and feeds into ./15-strategy-business-case-and-prioritization.md, where these technical considerations compete for the same prioritization slots as feature work.
Fundamentals
What a technical roadmap is for
A technical roadmap is a sequenced plan of the platform, infrastructure, and architecture work needed to keep delivering the product roadmap at an acceptable cost and risk — it is not a second product roadmap, and it is not a wish list of things engineers find interesting. The test for whether something belongs on a technical roadmap is whether it changes what the team can deliver, how fast, how safely, or how expensively. A migration that removes a scaling ceiling before the product needs that scale belongs on it. A rewrite that makes the code “nicer” but doesn’t change what the team can ship does not — however tempting it is to frame it that way.
A useful mental model is that the product roadmap describes what value ships, and the technical roadmap describes what has to be true about the system for that value to ship on schedule and stay cheap to operate. Good technical roadmaps are therefore derived, not invented in isolation: for each major theme on the product roadmap over the next two to four quarters, ask what the system needs to be able to do, and work backward to the platform investments that make it true. If the product roadmap includes expanding into three new regions, the technical roadmap needs multi-region data residency and latency work well before that quarter arrives, not during it.
Sequencing platform investment vs. feature work
The perennial tension is that platform work rarely has a visible, dated business outcome of its own — it enables outcomes rather than being one — while feature work almost always does. Left to compete on visibility alone, platform work loses every time, until the system finally can’t absorb another feature without breaking, at which point it gets funded in a panic, at a worse cost than if it had been planned. The EM’s job is to prevent that panic by making platform investment a scheduled, visible line item rather than a reactive scramble.
A few sequencing principles hold up well in practice:
- Front-load foundational work that gets more expensive with time. A data model change is cheap when the table has a thousand rows and painful when it has a billion. If a foundational decision is going to be revisited eventually, revisit it while it’s still cheap.
- Sequence platform work just ahead of the feature work it unblocks, not so early that it sits unused and drifts out of sync with real usage, and not so late that features stall waiting for it. This requires the EM to actually understand the product roadmap’s shape, not just its next sprint.
- Batch platform work with adjacent feature work when possible, so a team touching a subsystem for a feature also pays down debt or hardens that subsystem in the same pass — this is far cheaper than a dedicated project later, and much easier to get funded because it rides alongside something with a visible outcome.
- Protect a minimum floor of platform capacity even when the product roadmap is under pressure. If platform investment only ever happens in the leftover time after features, it will never happen, because there is never leftover time in a healthy, ambitious organization. See the capacity-allocation discussion below for how to make this concrete.
Communicating a technical roadmap to non-technical stakeholders
A technical roadmap that only technical people understand is a roadmap that will lose every prioritization fight it’s in, because the people making trade-off calls above the EM’s level — product leaders, finance, sometimes the executive team — cannot fund what they cannot understand the value of. The single biggest communication failure is presenting platform work in implementation language (“migrate off the monolith,” “upgrade the message queue”) instead of outcome language (“cut incident rate in checkout by half,” “unblock the ability to launch in the EU without a six-month lead time”). The technical roadmap needs a translation layer, every time it leaves a room of engineers.
Three habits make this translation stick. First, attach a business consequence to every roadmap item — not a technical description, a sentence about what breaks, slows down, or becomes impossible if the work doesn’t happen, framed in terms stakeholders already care about (cost, speed, risk, customer-facing reliability). Second, use analogies stakeholders already trust — comparing deferred infrastructure work to deferred maintenance on a building or a vehicle (it doesn’t stop the building from being used, but the cost of ignoring it compounds and eventually becomes an emergency) reliably lands better than any architecture diagram. Third, keep a visible, dated roadmap artifact — even a single slide with swimlanes for “features,” “platform,” and “risk reduction” — so platform work is seen sitting in the same plan as everything else, not off in a separate document that’s easy to deprioritize because it’s easy to forget. See ./13-stakeholder-communication-and-reporting.md for the broader set of techniques for tailoring technical communication to different audiences.
The technical debt metaphor, properly understood
Ward Cunningham coined “technical debt” in 1992 to explain, to a business audience, why shipping code fast without also refining the underlying design creates an obligation that has to be paid later, one way or another — not to give engineers license to write bad code and call it a strategy. His own explanation, offered years afterward because the metaphor had already drifted from his intent, was explicit that debt itself isn’t the problem: taking on debt to ship sooner can be a perfectly sound decision, as long as it’s paid down promptly, the same way a business loan can fund growth as long as it’s serviced. What Cunningham objected to was code that never gets refactored toward what was actually learned while building it — debt taken on and then never repaid, quietly compounding.
Martin Fowler later refined this into the Technical Debt Quadrant, which separates debt along two independent axes: whether it was deliberate or inadvertent, and whether it was prudent or reckless. The combination matters far more than the raw existence of debt, because it changes what the right response is.
| Reckless | Prudent | |
|---|---|---|
| Deliberate | ”We don’t have time for design,” taken as a habitual excuse rather than a genuine trade-off | ”We must ship now and deal with the consequences” — a conscious, informed trade-off to hit a real deadline |
| Inadvertent | ”What’s layering?” — debt from simply not knowing better | ”Now we know how we should have done it” — debt that only becomes visible in hindsight, after the team has learned more about the problem |
Read across the quadrant, and the practical implication is that debt is not inherently bad and is sometimes the correct engineering decision. A team that deliberately and prudently ships a simplified implementation to hit a genuine market or contractual deadline, fully aware of what was skipped and with a plan to revisit it, has made a good call — the alternative (missing the deadline to build the ideal version) may well have been worse for the business. Reckless debt, by contrast — whether deliberate (“we never have time for this”) or inadvertent (simply not knowing better) — is the kind that erodes a codebase and a team’s ability to move, because it isn’t a considered trade-off at all.
Why the real problem is untracked debt
If deliberate, prudent debt can be the right call, the actual danger isn’t debt — it’s debt nobody remembers taking on. Financial debt has a lender who sends a statement every month; technical debt has no such enforcement mechanism unless the team builds one. Left untracked, debt doesn’t announce itself: it just makes every subsequent change slightly slower and slightly riskier, in a way that’s nearly impossible to attribute to any one decision, until the team is paying a heavy “debt tax” on every unit of feature work without being able to say why.
This is the crux of the whole topic, and it reframes the manager’s job away from “eliminate technical debt” — an impossible and not even desirable goal — toward “make sure every piece of debt is a known, deliberate, tracked decision with an owner and a rough plan,” which is achievable and is where the rest of this note focuses.
Key Concepts
Making technical debt visible and prioritizable
Debt that lives only in engineers’ heads competes for nothing, because it never enters any prioritization conversation — it just gets silently re-discovered, and re-suffered, every time someone touches that part of the code. The fix is mechanical: give debt the same visibility infrastructure that features already have.
A debt register is the simplest version of this: a living list (a backlog label, a dedicated board, or even a shared document, depending on team size) where each entry records what the shortcut was, why it was taken, what it would cost to fix now versus later, what it’s currently costing the team (slower delivery in a specific area, a recurring class of bugs, an on-call burden), and who owns tracking it. The register matters less as a specific tool and more as a discipline: nothing goes into “we took a shortcut” territory without also going into the register, as a matter of team norm, not manager enforcement.
Tagging debt directly in the backlog — a tech-debt label, a dedicated debt epic, or a lightweight severity tag — keeps it inside the same planning and prioritization tool the team already uses, rather than in a separate system that’s easy to forget exists. This also makes debt reportable: a manager can pull up “how much labeled debt do we have, and how old is the oldest item” as an actual number, rather than a vague, perpetually-true feeling that “we should really clean this up sometime.”
The step that makes the biggest practical difference, though, is allocating a fixed percentage of capacity to debt paydown — commonly somewhere in the 10-20% range, though the right number depends on the system’s age and how much debt has already accumulated — rather than relying on “we’ll get to it when things calm down.” Things never calm down in a healthy, growing product; relying on slack time to pay down debt means debt paydown competes with feature work on the feature work’s terms, and loses every time, because feature work always has a nearer, more visible deadline. A protected percentage converts debt paydown from a hope into a standing commitment that survives contact with a busy roadmap, the same way a fixed savings deduction survives contact with a tempting purchase in a way that “I’ll save whatever’s left over” never does.
| Approach | What it does | Failure mode if skipped |
|---|---|---|
| Debt register | Records what was skipped, why, and its cost | Debt is re-discovered painfully, with no institutional memory of why it exists |
| Backlog tagging | Makes debt visible in the same planning tool as features | Debt lives in a separate, easily-ignored document or nobody’s head |
| Fixed capacity allocation | Guarantees paydown happens on a cadence | Paydown only happens in “spare time,” which never reliably arrives |
| Debt review cadence | Periodically re-prioritizes the register as context changes | Old, no-longer-relevant debt items linger and crowd out ones that now matter |
Technical risk assessment
Debt is risk that has already been incurred; risk assessment is about the debt — or worse, the outage, the missed deadline, the failed migration — that hasn’t happened yet but might. Before committing a team to a project, it’s worth deliberately asking where things could go wrong, rather than discovering it mid-project when the cost of discovery is highest. The common sources of technical risk worth checking for every non-trivial project include:
- Unfamiliar technology — the team is using a language, framework, or platform it hasn’t shipped with before, so estimates are based on guesses rather than experience.
- Tight or externally-fixed timelines — a deadline driven by a contract, a regulation, or a marketing commitment, which removes the option to simply extend the schedule if things go wrong.
- Key-person dependency — critical knowledge concentrated in one person, so their unavailability (illness, attrition, even vacation) stalls the whole effort.
- Third-party and vendor reliability — the project depends on an external API, service, or vendor whose uptime, roadmap, or continued existence the team doesn’t control.
- Unclear or shifting requirements — the team is being asked to build against a specification that is likely to change, which risks throwing away work.
- Integration complexity — the work touches many existing systems, each of which is an opportunity for an unexpected interaction.
A simple and effective way to turn this list into a decision tool is the same likelihood × impact scoring pattern used in security risk assessment — see ../devsecops/en/06-threat-modeling-and-risk-assessment.md for the fuller treatment of this pattern applied to security threats specifically. For each identified risk, score likelihood (how probable is it that this actually bites the team) and impact (how bad is it if it does) on a simple scale, typically 1-5, and multiply them to get a rough priority score.
| Risk | Likelihood (1-5) | Impact (1-5) | Score | Mitigation |
|---|---|---|---|---|
| Unfamiliar framework for the whole team | 4 | 3 | 12 | Time-boxed spike or pilot before committing the full team; bring in outside expertise for review |
| Hard external deadline (regulatory) | 2 | 5 | 10 | Build in a buffer; identify a reduced-scope fallback early |
| Single engineer knows the legacy billing system | 4 | 4 | 16 | Pair on the work now; document as it’s touched; do not let this go untouched for another cycle |
| Third-party payment provider outage history | 2 | 4 | 8 | Add a fallback provider or a graceful-degradation path |
The score is not a precise measurement — it’s a forcing function that gets risks named and compared on the same footing, rather than left as vague unease that never quite makes it into a planning conversation. High-score items deserve an explicit mitigation before the project is committed to a date; low-score items can be accepted and simply watched. The output of this exercise should feed directly into the schedule and staffing conversation, not sit in a document nobody revisits — a project with a 16-scored key-person risk should not be scheduled the same way as one without it.
Build vs. buy evaluation
Every non-trivial capability raises the question of whether to build it in-house or acquire it (as a vendor product, a managed service, or an open-source dependency), and getting this wrong in either direction is expensive: building undifferentiated infrastructure that a vendor already does well wastes engineering time the business needed elsewhere, while buying something that turns out to be a core differentiator locks the company’s competitive advantage behind someone else’s product roadmap.
The most useful lens is the distinction, popularized in this context by Amazon, between core competency and undifferentiated heavy lifting. If a capability is part of what makes the product genuinely distinctive to customers — the thing customers would notice and care about if it were worse — it’s a strong candidate to build and own, because owning it is owning the differentiation itself. If a capability is necessary but invisible to the customer and not a source of competitive advantage — authentication, payment processing, email delivery, container orchestration — it’s usually undifferentiated heavy lifting, and building it in-house means spending scarce engineering time reinventing something a specialist vendor has already spent years perfecting.
Beyond that core lens, a few other factors consistently belong in the decision:
| Factor | Favors build | Favors buy |
|---|---|---|
| Strategic differentiation | Capability is core to competitive advantage | Capability is necessary but invisible to customers |
| Total cost of ownership | Vendor pricing scales worse than in-house cost at the company’s volume | In-house cost (engineering time, ongoing maintenance, on-call burden) exceeds vendor pricing once fully loaded |
| Time to market | Team has the exact expertise and time needed, and speed-to-build beats speed-to-integrate | A working solution is needed sooner than an in-house build could realistically deliver |
| Customization needs | Requirements are specific enough that no vendor product fits without heavy workarounds | Standard functionality covers the need; edge cases are rare enough to live with |
| Vendor lock-in risk | Switching cost or vendor dependency risk is unacceptable for a capability this central | Lock-in risk is acceptable given the vendor’s stability, or mitigated by an abstraction layer / open standard |
| Ongoing maintenance burden | Team wants to own the maintenance and evolution of the capability long-term | The team doesn’t want a permanent maintenance burden for something outside its core focus |
| Security & compliance | In-house control is required by regulation or contract | Vendor already carries relevant certifications the team would need years to obtain |
Total cost of ownership deserves special emphasis because it’s the factor most often estimated wrong, almost always in the direction of underestimating build. An in-house build’s headline cost is the initial engineering time, but its real cost also includes every subsequent bug fix, security patch, on-call page, dependency upgrade, and the opportunity cost of the engineers who could have been building product instead — costs that a vendor has already amortized across many customers. A useful discipline is to price a build decision not at initial cost, but at a multi-year total, including a realistic maintenance-headcount fraction, before comparing it to a vendor quote.
Legacy system retirement
Legacy systems persist for reasons that are rational at the individual-decision level even when the aggregate outcome is clearly bad for the organization. Two forces dominate. The first is fear: a system nobody fully understands anymore, built by people who’ve since left, is terrifying to touch, because any change might break something whose purpose isn’t documented and whose failure mode is unknown — so the path of least resistance is always to build one more workaround on top rather than risk the retirement. The second is sunk cost: a system that took years and significant budget to build feels wasteful to retire, even when the honest comparison is entirely forward-looking (what does it cost to keep running versus what does it cost to replace), because the sunk investment is emotionally, if not economically, relevant to the people who built it.
The practical antidote to both forces is to retire the system incrementally rather than all at once, so no single step requires fully understanding — or fully trusting — the whole legacy system before starting. The strangler fig pattern (named for the vine that grows around a host tree, gradually taking over its structural role until the original tree can be removed) is the standard approach: new functionality is built around the legacy system’s edges, incoming traffic is gradually redirected from old to new capability piece by piece, and the legacy system’s footprint shrinks continuously rather than all at once, until what’s left can be safely switched off. See ../backend/en/14-software-architecture.md for the fuller architectural treatment of this pattern and how it compares to a big-bang rewrite. The strangler fig pattern is what makes retirement politically and operationally feasible: it never requires a large, risky cutover date, it produces continuous, demonstrable progress that’s easy to report on, and it lets the team back out of any single step without having burned the whole migration.
Once a legacy component’s traffic has genuinely been fully migrated, decommissioning it is its own project with its own risk of quiet failure, and benefits from a checklist rather than an assumption that “it’s not used anymore, we can just turn it off”:
- Confirm zero traffic/usage over a representative window (not just a quiet Tuesday) using real monitoring, not developer memory.
- Identify and migrate or archive any data still living only in the legacy system.
- Check for indirect dependents — reporting jobs, other services, scheduled batch jobs, or even other teams’ code — that call the legacy system without being obviously connected to its primary traffic path.
- Turn the system off in a reversible way first (e.g., disable writes, return errors, or redirect at the network layer) before physically deleting infrastructure, and hold that state for an agreed observation period.
- Communicate the shutdown date broadly, including to teams that might have an undocumented dependency, well before the date arrives.
- Remove the infrastructure, credentials, monitoring, and on-call rotations only after the observation period passes cleanly.
- Document what was retired and why, for institutional memory the next time someone asks “wait, didn’t we used to have a system for this?”
Tool transitions and technology adoption
Teams fail at technology adoption in one of two opposite ways, and both are common enough to name. The first is chasing every new framework, database, or tool as soon as it trends, which fragments the team’s expertise across too many technologies, makes hiring and onboarding harder, and often trades a known set of problems for an unknown set that turns out to be worse. The second is refusing to modernize at all, which eventually leaves the team stuck on tooling that’s unsupported, insecure, or simply so far behind current practice that hiring becomes difficult because candidates don’t want to work in it.
A lightweight adoption framework avoids both extremes by treating new technology as a hypothesis to be tested cheaply before it’s trusted broadly:
- Identify a real problem first, not a technology looking for a use case — the trigger should be “our current approach is causing X pain,” not “I read about this tool and it looks cool.”
- Pilot small, on a low-stakes, bounded piece of real work — not a toy project, which won’t surface real integration or operational issues, but also not a business-critical system, which makes the pilot too risky to be honest about failure.
- Set success criteria before starting the pilot, not after — what would “this worked” and “this didn’t work” concretely look like, so the outcome isn’t judged by whichever result the team is already emotionally invested in.
- Measure the pilot honestly against those criteria, including the costs that don’t show up in a demo: operational burden, learning curve for the rest of the team, quality of documentation and community support, and how it behaves under failure, not just under the happy path.
- Decide explicitly to scale or reject, and, importantly, actually reject some pilots — a framework where every pilot quietly graduates to production regardless of outcome isn’t evaluating anything, it’s just adoption with extra steps.
- If scaling, plan the migration deliberately — including who owns the transition period where both old and new tools are in use, and a target date for the old tool to be fully retired, so “adopting” something new doesn’t just mean permanently supporting two of everything.
Best Practices
Treat these as habits to build into the team’s regular cadence rather than one-time exercises, since roadmap, debt, and risk all decay if reviewed only once:
- Maintain a live technical roadmap document alongside the product roadmap, reviewed and re-sequenced on the same cadence as product planning, not as an annual afterthought.
- Give every debt item an owner and a rough cost estimate — a debt register with no owner is a to-do list nobody’s accountable for.
- Protect a fixed percentage of every planning cycle for debt paydown, and defend it in planning conversations the same way a deadline commitment would be defended.
- Run a lightweight likelihood × impact risk pass before committing any project with an external deadline, unfamiliar technology, or a single point of failure in people or vendors.
- Default to buy for undifferentiated capability and build for genuine competitive differentiation, and re-litigate the total cost of ownership honestly, including maintenance headcount, before assuming build is cheaper.
- Retire legacy systems incrementally with a strangler fig approach rather than betting everything on a single cutover, and use a decommissioning checklist rather than trusting memory that a system is unused.
- Pilot new tools on bounded, real work with success criteria set in advance, and be willing to reject a pilot — a technology adoption process that never says no isn’t actually evaluating anything.
- Report technical roadmap items to non-technical stakeholders in outcome language, not implementation language, and keep them visible in the same plan as feature work rather than in a separate, easily-deprioritized document.
References
- Ward Cunningham, “Ward Explains Debt Metaphor” — the original 1992 explanation of the technical debt metaphor, from the person who coined it.
- Martin Fowler, “TechnicalDebt” — overview of the metaphor and its practical implications.
- Martin Fowler, “TechnicalDebtQuadrant” — the deliberate/inadvertent, prudent/reckless framework referenced above.
- Martin Fowler, “StranglerFigApplication” — the incremental legacy-replacement pattern.
./09-technical-decision-making-and-architecture.md— how architecture decisions concretely embody roadmap, debt, and risk trade-offs.../devsecops/en/06-threat-modeling-and-risk-assessment.md— the likelihood × impact risk-scoring pattern applied to security threats.../backend/en/14-software-architecture.md— deeper architectural treatment of the strangler fig pattern and migration strategies../15-strategy-business-case-and-prioritization.md— how technical roadmap items compete for priority alongside feature work.