Lê Duy Khương (Daniel)

Chuỗi: docs-as-code

Năng suất & công cụ dev

Tại sao Documentation-as-Code — bài học từ một lỗi không ai thấy suốt cuối tuần

Minh làm đúng tài liệu. Tài liệu sai. Hàng trăm khách không được hoàn tiền suốt cuối tuần. Câu chuyện về vì sao tài liệu phải sống cùng mã nguồn — coi docs như code: plain text, version control, review qua pull request, CI tự kiểm tra.

2026-06-249 phút đọcVI

Đây là bài quan trọng nhất. Nếu cả khóa bạn chỉ đọc một bài, hãy đọc bài này — nó cho bạn lý do để thay đổi cách cả đội viết tài liệu. Bốn bài sau chỉ là cách làm; bài này là vì sao.

Tóm tắt docs-as-code: 4 thói quen cốt lõi + 3 cái bẫy documentation rot


Một lỗi không ai nhìn thấy suốt cả cuối tuần

Chiều thứ Sáu. Minh — lập trình viên mới của đội Booking (đặt chỗ) — cần làm tính năng hoàn tiền cho khách hủy đặt chỗ.

May quá, super-app đã có sẵn API hoàn tiền dùng chung do đội nền tảng (đội super-app, lo các dịch vụ back-end chung cho mọi mini-app) cung cấp. API này lại có hẳn tài liệu trên Confluence (wiki nội bộ). Minh mở tài liệu, làm đúng từng chữ: gọi POST /refunds với hai trường order_idamount. Chạy thử trên máy: ổn. Ship.

Thứ Hai, tổng đài cháy máy. Hàng trăm khách hủy chỗ cuối tuần nhưng không ai nhận được tiền hoàn.

Chuyện gì đã xảy ra?

Ba tuần trước, đội nền tảng thêm một trường bắt buộc mới vào API hoàn tiền dùng chung: reason (lý do hoàn tiền), để tuân thủ một quy định mới. Họ sửa code, sửa test, ship lên production. Nhưng tài liệu trên Confluence thì... không ai đụng tới. Nó vẫn ghi API chỉ cần order_idamount.

Minh làm đúng tài liệu. Tài liệu sai. Mọi yêu cầu hoàn tiền của Booking bị API trả về lỗi 400 Bad Request — và vì code Booking không kiểm tra kỹ, lỗi bị nuốt im lặng. Tiền không được hoàn, không một cảnh báo nào, suốt cả cuối tuần.

Kết quả: nửa ngày truy ngược nguyên nhân, một cuộc họp căng thẳng giữa hai đội, và những khách hàng mất niềm tin. Nhưng câu hỏi đau nhất không phải "ai viết sai code". Mà là:

"Tại sao tài liệu lại nói một đằng, code làm một nẻo?"

Minh không sai. Đội nền tảng cũng không lười — họ có cập nhật, chỉ là cập nhật code mà quên tài liệu. Thủ phạm thật sự là một thứ vô hình: không có gì buộc tài liệu phải đi cùng code. Khi cập nhật tài liệu là một việc rời, tùy hứng, nó sẽ luôn bị bỏ lại phía sau. Hiện tượng này có tên: documentation rot (tình trạng tài liệu cũ dần và lệch khỏi code đến mức gây hiểu sai).

💡 Hình dung: Tài liệu tách rời code giống tấm bản đồ giấy của một thành phố đang xây. Bản đồ in hôm nay, ngày mai đã có con đường mới mà nó không biết. Bạn càng tin bản đồ cũ, bạn càng lạc sâu.

Tin tốt: có một cách làm khiến câu chuyện của Minh gần như không thể xảy ra. Đó là nội dung cả khóa học này. Và nó bắt đầu từ một ý tưởng đơn giản đến bất ngờ.


Bài học này sẽ cho bạn

  • Hiểu documentation rot là gì và vì sao nó nguy hiểm — nhất là khi nhiều đội làm việc cùng nhau.
  • Nắm 4 thói quen biến tài liệu thành thứ luôn đồng bộ với code.
  • Thấy hình dạng một repo super-app (siêu ứng dụng) tổ chức theo Docs-as-Code trông ra sao.
  • Hiểu vì sao trong super-app / mini-app, tài liệu lỗi thời không chỉ phiền mà còn phá vỡ niềm tin giữa các đội.
  • Nhận ra 3 cái bẫy khiến mọi nỗ lực tài liệu thất bại.
  • Tự soi tài liệu của đội mình qua một lăng kính mới: nguy cơ documentation rot.

Ý tưởng cốt lõi: coi tài liệu như mã nguồn

Suốt nhiều năm, đội kỹ sư đã xây cả một bộ kỷ luật để code không bị hỏng dần. Cụ thể: viết ở dạng text, lưu trong Git, review trước khi gộp, và để máy tự kiểm tra. Code hiếm khi âm thầm sai trong im lặng như tài liệu của Minh — vì nó được canh giữ bởi cả một quy trình.

Documentation-as-Code (coi tài liệu như mã nguồn) chỉ là một câu hỏi: Nếu bộ kỷ luật đó giữ code khỏi hỏng, sao ta không dùng luôn nó cho tài liệu?

Thế là tài liệu cũng được đối xử y như code, qua 4 thói quen dưới đây. Hãy để ý: từng thói quen bịt một lỗ hổng đã hại Minh.

1. Plain text — viết bằng Markdown. Tài liệu viết bằng Markdown (ngôn ngữ đánh dấu văn bản đơn giản: # cho tiêu đề, - cho danh sách). File là text thuần, mở bằng bất kỳ trình soạn thảo nào, và máy so sánh được từng dòng. Không còn file .docx nhị phân mà bạn chẳng biết "ai vừa đổi gì".

2. Version control — sống trong Git. Tài liệu nằm trong Git, cùng kho mã với code nó mô tả. Mỗi lần sửa là một commit (một mốc ghi vào lịch sử). Bạn luôn trả lời được: ai sửa, sửa gì, khi nào, và vì sao.

3. Review qua pull request. Mọi thay đổi tài liệu đi qua pull request / PR (đề nghị gộp thay đổi để đồng đội xem trước khi nhập vào nhánh chính). Nếu đội cung cấp API sửa API trong một PR mà quên tài liệu, người review sẽ hỏi ngay tại cửa: "Đổi API rồi, tài liệu đâu?"

4. CI tự kiểm tra. CI (Continuous Integration — máy tự chạy kiểm tra mỗi khi có thay đổi) soi tài liệu giúp bạn: link có gãy không, định dạng có đúng không, thuật ngữ có nhất quán không. Sai thì CI báo đỏ và chặn lại — y như test code thất bại.

Mấu chốt nằm ở chỗ: 4 thói quen này không rời rạc — chúng khóa vào nhau thành một vòng. Tài liệu sống cạnh code (1, 2), nên nó lọt vào cùng PR khi code đổi (3), và máy canh ở cửa (4). Bạn không thể quên cập nhật tài liệu, vì cả quy trình sẽ nhắc bạn. Lỗi của Minh trở thành chuyện gần như không xảy ra được.

💡 Một câu để nhớ: Nếu code là "phần mềm cho máy chạy", thì tài liệu là "phần mềm cho con người chạy". Cả hai đều cần được kiểm thử, review và bảo trì. Docs-as-Code chỉ thừa nhận điều đó.


Vì sao điều này đáng để cả đội thay đổi

Khi tài liệu sống như code, ba điều giá trị xuất hiện — và cái đầu tiên là cả lý do tồn tại của phương pháp này.

① Tài liệu luôn đồng bộ — vì đi chung một PR với code.

Đây là lợi ích lớn nhất, lớn hơn tất cả phần còn lại cộng lại. Khi đội nền tảng thêm trường reason, họ sửa code sửa tài liệu trong cùng một pull request. Người review thấy cả hai cùng lúc.

Tài liệu lỗi thời không còn chỗ để âm thầm tồn tại. Câu chuyện của Minh sẽ kết thúc khác: anh đọc tài liệu đã đúng, thêm reason, xong.

② Ai cũng sửa được — rào cản gần như bằng không. Thấy tài liệu sai? Sửa một file text, mở PR, 2 phút. Không cần xin quyền Confluence, không cần "liên hệ người quản lý trang". Tài liệu thành tài sản chung mà cả đội cùng chăm.

③ Tài liệu lên hàng "sản phẩm hạng nhất". Khi tài liệu đi qua review và CI như code, nó hết là "việc làm cho có". Định nghĩa "xong" của một tính năng giờ bao gồm cả tài liệu. Chưa cập nhật tài liệu = chưa xong.

Và trong super-app, đây không còn là "tốt nếu có"

Trong một ứng dụng đơn lẻ, documentation rot gây phiền. Trong một super-app (siêu ứng dụng chứa nhiều mini-app) như NovaApp — với Payments, Booking, Wallet, Rewards mỗi cái một đội — hậu quả nhân lên:

Đặc điểm super-appVì sao documentation rot nguy hiểm hơn
Nhiều đội đọc tài liệu của nhauBooking xài API hoàn tiền dùng chung (do đội nền tảng cung cấp). Tài liệu sai của đội cung cấp thành lỗi của đội tiêu dùng — đúng như Minh.
shell dùng chungMọi mini-app (ứng dụng nhỏ chạy trong super-app) dựa trên "hợp đồng" của shell (lớp vỏ chung). Tài liệu shell sai làm lệch tất cả mini-app cùng lúc.
Vòng đời lệch nhauWallet ship hằng tuần, Rewards hằng tháng. Khi nhịp khác nhau, tài liệu là ngôn ngữ chung duy nhất giữ mọi người đồng bộ. Nó sai là mất điểm tựa.

Trong super-app, tài liệu không phải "ghi chú cho riêng mình" — nó là contract (hợp đồng giao tiếp giữa các đội). Một hợp đồng sai phá vỡ niềm tin và gây lỗi dây chuyền. Đây chính là lúc Documentation-as-Code chuyển từ "nên có" thành "bắt buộc".

Hình dạng một repo super-app theo Docs-as-Code

Để dễ hình dung, đây là cách một repo super-app có thể được tổ chức. Tài liệu sống cạnh code và cạnh các hợp đồng, tất cả trong cùng một monorepo (một repo chứa nhiều thành phần):

nova-superapp/                          # Repo super-app (monorepo)
├── shell/                              # Lớp vỏ dùng chung (đội super-app)
│   ├── src/
│   └── docs/                           # tài liệu của shell
├── bff/                                # ★ Cổng API back-end dùng chung (đội super-app)
│   ├── src/
│   └── docs/                           # tài liệu API của BFF (gồm hoàn tiền)
├── mini-apps/
│   ├── payments/
│   │   ├── src/
│   │   └── docs/                       # tài liệu riêng của Payments (thanh toán/QR)
│   ├── booking/      # (src/ + docs/)
│   ├── wallet/       # (src/ + docs/)
│   └── rewards/      # (src/ + docs/)
├── contracts/                          # ★ Hợp đồng API giữa các đội (versioned)
│   ├── shell-to-miniapp.contract.md    #   shell ↔ mini-app
│   └── bff-refund.contract.md          #   mini-app ↔ BFF — nơi trường `reason` được khai báo
├── docs/                               # Tài liệu toàn super-app
│   └── product-docs/                   # kiến trúc BRD → PRD → FR → Story
├── .github/workflows/                  # CI: kiểm tra tài liệu + hợp đồng
└── README.md

Hai thư mục đáng chú ý. bff/BFF (Backend-for-Frontend — cổng API back-end dùng chung do đội super-app sở hữu). Các API mà nhiều mini-app cùng cần — như hoàn tiền (Booking hủy chỗ, Wallet hoàn vào ví) — sống ở đây, thay vì nhét trong một mini-app.

contracts/ là mảnh ghép biến câu chuyện Minh thành bài học khép kín: hợp đồng API giữa các đội sống ở đây dưới dạng tài liệu, versioned và review như code. Khi đội nền tảng thêm trường reason, họ buộc phải sửa bff-refund.contract.md trong cùng PR.

Hệ thống tự gọi đội Booking (đội tiêu dùng) vào review (bạn sẽ học cơ chế CODEOWNERS ở Lesson 4), và CI có thể kiểm tra xem code có khớp hợp đồng không. Minh được cảnh báo trước khi lỗi xảy ra — chứ không phải sau một cuối tuần cháy máy.

📌 Đây mới chỉ là bức phác thảo. Lesson 3 đào sâu kiến trúc tài liệu sản phẩm (product-docs/), còn Lesson 6 dựng lại toàn bộ chiến lược cho super-app — gồm cả contracts/, Git strategy và CI/CD đầy đủ. (Ngoài monorepo, super-app cũng có thể tách thành nhiều repo riêng — Lesson 6 so sánh hai cách.)


Bộ công cụ tối thiểu: ít hơn bạn nghĩ

Đừng để chữ "công cụ" làm bạn nản. Bộ tối thiểu chỉ có 4 mảnh, và hầu hết đội kỹ sư đã sẵn 3 trong số đó:

MảnhVai tròHọc sâu ở
MarkdownĐịnh dạng viết — text thuần, dễ đọc, so sánh được từng dòngLesson 2
GitQuản lý phiên bản — lịch sử, nhánhLesson 4
Pull requestReview — đồng đội duyệt trước khi gộpLesson 4
CITự kiểm tra — link, định dạng, nhất quánLesson 5

Bài này bạn chỉ cần nắm bức tranh lớn: 4 mảnh ghép phối hợp ra sao. Chưa cần thuộc cú pháp Markdown hay cấu hình CI — các bài sau dắt tay bạn từng bước.

📌 Bản chất Docs-as-Code là thói quen, không phải công cụ. Một đội chỉ với Markdown + Git + PR cơ bản đã chiếm 80% giá trị. Công cụ xịn hơn chỉ tối ưu 20% còn lại.


Ba cái bẫy khiến tài liệu vẫn thất bại

Công cụ là phần dễ. Phần khó là thói quen. Dưới đây là 3 cái bẫy (anti-pattern) phải nhận ra để né.

Bẫy 1 — Viết quá nhiều (over-documentation). Ghi lại cả những thứ code đã tự nói, tài liệu phình to, không ai đọc hết, và càng nhiều chữ càng nhiều thứ bị documentation rot. Lối ra: chỉ viết cái code không tự nói được — lý do thiết kế (vì sao thêm reason), hợp đồng giữa các đội, bối cảnh quyết định.

Bẫy 2 — Để tài liệu làm sau cùng (docs-as-afterthought). "Ship code trước, rảnh rồi viết tài liệu sau." Mà "rảnh" thì không bao giờ tới. Lối ra: tài liệu đi cùng PR với code, và là một phần của định nghĩa "xong".

Bẫy 3 — "Tài liệu là việc của người khác". Lập trình viên nghĩ đó là việc của người viết tài liệu; người viết tài liệu lại không hiểu sâu code. Quả bóng trách nhiệm bị đá qua lại đến khi không ai giữ. Lối ra: người gần code nhất sở hữu tài liệu phần đó — và Docs-as-Code làm điều này khả thi, vì sửa tài liệu giờ chỉ là sửa file text + mở PR.

BẫyDấu hiệuLối ra
Viết quá nhiềuTài liệu dài, lặp code, không ai đọcChỉ ghi cái code không nói được
Tài liệu làm sau cùngTài liệu luôn trễ hơn codeTài liệu vào cùng PR
"Việc của người khác"Không ai chịu trách nhiệmNgười gần code nhất sở hữu

💡 Cả tư duy mới gói trong một câu: "Tài liệu chưa cập nhật thì tính năng chưa xong." Khi cả đội thật lòng tin câu này, documentation rot gần như biến mất.


Repo này chính là một ví dụ sống

Điều hay nhất: chính bộ kit bạn đang đọc cũng được tổ chức theo Documentation-as-Code. Nó không chỉ nói về phương pháp — nó một ví dụ đang chạy. Bạn có thể mở ra xem như một mô hình mẫu:

Trong repoVai trò
docs/lessons/Các bài học (như file này) — Markdown, có frontmatter (khối metadata giữa hai dòng ---), sống trong Git
templates/product-docs/Bộ mẫu tài liệu sản phẩm theo vòng đời: 00-discovery/ (BRD), 01-product/ (PRD)... cho tới 04-delivery/
skills/docs-as-code-intro/Một skill (tri thức đóng gói cho trợ lý AI) tóm tắt chính phương pháp này

Mọi file ở đây đều là Markdown, đi qua Git, được review qua PR, và kiểm bởi CI (xem .github/workflows/). Học xong, bạn sao chép thẳng templates/product-docs/ để khởi động tài liệu cho dự án của mình.

📌 Mở thử templates/product-docs/00-discovery/brd/_TEMPLATE-brd.md để thấy một mẫu BRD (Business Requirements Document — tài liệu yêu cầu nghiệp vụ) thực tế trông ra sao.

📂 Muốn xem "đáp án" hoàn chỉnh trước? Mở examples/nova-superapp/ — một super-app NovaApp đã tài liệu hoá trọn vẹn, với đúng câu chuyện reason ở trên chảy xuyên từ BRD tới test. Các bài sau, mỗi bài tập đều trỏ về ví dụ này để bạn đối chiếu.


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

Mục tiêu: tập "nhìn" tài liệu qua lăng kính nguy cơ documentation rot — kỹ năng quan trọng nhất sau bài này.

Bước 1 — Liệt kê. Lập danh sách tài liệu hiện có của đội bạn. Chưa có đội thật? Hãy đóng vai phụ trách mini-app Booking của NovaApp và liệt kê tài liệu giả định. Ví dụ: tài liệu API đặt chỗ, hướng dẫn tích hợp hoàn tiền với BFF, mô tả luồng hủy chỗ, ghi chú cấu hình shell.

Bước 2 — Chấm nguy cơ. Mỗi tài liệu gán Cao / Trung / Thấp kèm lý do ngắn. Gợi ý:

  • Mô tả thứ đổi thường xuyên (API, hợp đồng giữa các đội) → nguy cơ Cao.
  • Sống tách rời code (Word, Confluence, ổ đĩa chung) → cộng thêm nguy cơ.
  • Không ai sở hữu rõ → cộng thêm nguy cơ.
  • Mô tả thứ ổn định, ít đổisống cạnh code → nguy cơ Thấp.

Bước 3 — Lập bảng.

Tài liệuNơi lưu hiện tạiNguy cơLý do
API hoàn tiền (tích hợp Booking↔BFF)ConfluenceCaoĐổi thường xuyên, tách rời code, liên đới 2 đội
............

Bước 4 — Suy ngẫm. Chọn một tài liệu nguy cơ Cao, viết 2–3 câu: nếu chuyển nó sang Docs-as-Code (Markdown + Git + PR + CI), câu chuyện của Minh sẽ thay đổi thế nào?


Tóm tắt

  • documentation rot xảy ra khi tài liệu sống tách rời code và không gì buộc chúng đồng bộ. Câu chuyện Minh ↔ trường reason cho thấy nó gây lỗi thật, vượt ranh giới đội.
  • Documentation-as-Code coi tài liệu như mã nguồn qua 4 thói quen khóa vào nhau: plain text (Markdown) · version control (Git) · review qua pull request · CI tự kiểm tra.
  • Lợi ích lớn nhất: tài liệu + code đi chung một PR → luôn đồng bộ. Kèm theo: ai cũng sửa được, và tài liệu thành sản phẩm hạng nhất.
  • Trong super-app/mini-app: tài liệu là contract giữa các đội — sai một chỗ, vỡ dây chuyền. contracts/ trong repo là nơi giữ hợp đồng đó an toàn. Phương pháp này từ "nên có" thành "bắt buộc".
  • Tránh 3 bẫy: viết quá nhiều · để tài liệu làm sau · "việc của người khác". Kim chỉ nam: "Tài liệu chưa cập nhật thì tính năng chưa xong."
  • Repo này là ví dụ sống: docs/lessons/, templates/product-docs/, skills/docs-as-code-intro/.

Đọc tiếp

  • Lesson 2 — Viết bằng Markdown: giờ bạn đã có lý do, bài sau cho bạn công cụ đầu tiên — viết tài liệu sạch, nhất quán bằng Markdown.
  • Thử ngay: mở templates/product-docs/ và đọc một README bất kỳ — để cảm nhận một kiến trúc tài liệu sống động.

Bài viết này là một bài học trong bộ giáo cụ mã nguồn mở docs-as-code-kit (CC BY 4.0) tôi đang xây — dạy docs-as-code qua một super-app hư cấu "NovaApp". Bạn có thể đọc trọn bộ, xem ví dụ chạy được, hoặc cài làm plugin Claude Code tại GitHub: leduykhuong-daniel/OSS-docs-as-code-kit.

LDK

Le Duy Khuong

AI Transformation & Digital Strategy. Writing about agentic systems, engineering leadership, and building in public.