Menu
ESC

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

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

Đang tải...

Bài 4 — ADR (Architecture Decision Record) — Cấu trúc

Architecture Decision-Making Bài 4/60

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

Hãy tưởng tượng bạn vừa gia nhập một công ty fintech ở TP.HCM. Trong tuần đầu tiên, bạn phát hiện hệ thống thanh toán đang dùng một message broker khá "lạ", cấu hình database chia shard theo một cách khó hiểu, và tầng cache thì có tới ba lớp chồng lên nhau. Bạn hỏi người trong team: "Tại sao lại làm thế này?". Câu trả lời quen thuộc đến đau lòng: "Không biết, hồi đó anh A làm, giờ anh A nghỉ rồi."

Đây chính là căn bệnh mãn tính của mọi hệ thống phần mềm sống lâu: kiến thức về quyết định bị bốc hơi. Code thì còn đó, nhưng lý do đằng sau mỗi quyết định kiến trúc thì biến mất theo người đã rời đi, theo những cuộc họp không ai ghi biên bản, theo các tin nhắn Slack đã bị cuộn trôi. Ba năm sau, không ai dám sửa vì sợ phá vỡ một ràng buộc vô hình nào đó mà chẳng ai còn nhớ.

ADR — Architecture Decision Record — ra đời để chữa đúng căn bệnh này. Đây là công cụ đơn giản đến mức nhiều người xem thường, nhưng lại là một trong những thói quen phân biệt rõ ràng nhất giữa một kỹ sư senior với một Principal Architect. Ở bài này, chúng ta tập trung riêng vào cấu trúc của một ADR — nó gồm những phần nào, mỗi phần viết gì, và tại sao từng phần lại tồn tại. (Việc khi nào cần viết ADR sẽ được bàn kỹ ở Bài 5, nên ở đây bạn cứ tạm hiểu là chúng ta đang có một quyết định đáng ghi lại.)

Nắm vững cấu trúc ADR không chỉ giúp bạn ghi chép tốt hơn. Nó rèn cho bạn cách tư duy về một quyết định: buộc bạn phải nói rõ bối cảnh, cân nhắc các phương án, và thành thật với những hệ quả — kể cả những hệ quả xấu.

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

ADR là gì

Khái niệm ADR được Michael Nygard giới thiệu chính thức trong một bài viết năm 2011 ("Documenting Architecture Decisions"). Định nghĩa của ông rất gọn:

> Một ADR là một tài liệu ngắn ghi lại một quyết định kiến trúc quan trọng, cùng với bối cảnh (context) dẫn đến quyết định đó và những hệ quả (consequences) mà nó mang lại.

Hãy để ý ba chữ được nhấn mạnh: quyết định, bối cảnh, hệ quả. Đó chính là bộ ba xương sống của mọi ADR. Nygard cố tình giữ nó ngắn — một ADR chuẩn thường chỉ dài một trang, đọc trong 3–5 phút. Nó không phải là tài liệu thiết kế chi tiết, không phải sơ đồ 20 trang, không phải RFC dài dằng dặc. Nó là một mẩu ghi chú có kỷ luật về một lựa chọn.

Một điểm cực kỳ quan trọng cần hiểu ngay từ đầu: ADR là bất biến (immutable). Khi một ADR đã được chấp thuận, bạn không sửa nội dung của nó nữa. Nếu sau này quyết định thay đổi, bạn viết một ADR mới và đánh dấu ADR cũ là "Superseded" (bị thay thế). Lý do: ADR ghi lại quyết định tại một thời điểm với thông tin lúc đó. Nó là một mục trong nhật ký lịch sử, không phải một trang wiki có thể chỉnh sửa liên tục. Chính tính bất biến này biến tập hợp ADR thành một "dòng thời gian tư duy" của cả hệ thống.

Các trường (field) chuẩn trong một ADR

Cấu trúc kinh điển theo Nygard gồm năm phần chính. Nhiều team bổ sung thêm một vài trường, nhưng năm phần này là bộ khung tối thiểu mà bạn nên thuộc lòng.

1. Title (Tiêu đề) — Một câu ngắn mô tả quyết định, thường đánh số tuần tự. Ví dụ: "ADR-012: Sử dụng PostgreSQL làm cơ sở dữ liệu chính cho dịch vụ đơn hàng". Tiêu đề nên là một cụm danh từ mô tả quyết định, không phải một câu hỏi. Số thứ tự giúp tham chiếu chéo dễ dàng ("xem ADR-007").

2. Status (Trạng thái) — Vòng đời của một ADR đi qua các trạng thái: Proposed (đề xuất), Accepted (đã chấp thuận), Rejected (bị từ chối), Deprecated (không còn khuyến nghị), hoặc Superseded by ADR-XXX (đã bị thay thế bởi ADR khác). Trạng thái cho người đọc biết ngay tài liệu này có còn hiệu lực hay không.

3. Context (Bối cảnh) — Đây là linh hồn của ADR và cũng là phần khó viết nhất. Bạn mô tả tình huống buộc phải ra quyết định: các ràng buộc kỹ thuật, yêu cầu nghiệp vụ, giới hạn về nhân lực, thời gian, ngân sách, và các lực đẩy (forces) đang giằng co nhau. Điều quan trọng: viết Context một cách trung lập, mô tả sự thật và áp lực, chưa nói đến giải pháp. Người đọc sau này phải hiểu được "hồi đó tình hình là như vậy".

4. Decision (Quyết định) — Nói rõ bạn đã chọn gì, viết ở thể chủ động và dứt khoát: "Chúng tôi sẽ dùng...". Đây là phần trả lời câu hỏi "cuối cùng làm gì?". Ngắn gọn, không mơ hồ.

5. Consequences (Hệ quả) — Sau khi ra quyết định này, thế giới thay đổi ra sao? Phần này phải ghi cả hệ quả tích cực, tiêu cực, và trung tính. Đây là chỗ nhiều người viết ADR thất bại: họ chỉ liệt kê ưu điểm như đang viết quảng cáo. Một ADR tốt thành thật ghi lại cả cái giá phải trả — vì chính những dòng "tiêu cực" này mới cứu đội ngũ tương lai khỏi vấp phải bất ngờ.

Các trường mở rộng thường gặp

Ngoài năm phần trên, thực tế nhiều team thêm:

  • Options considered / Alternatives (Các phương án đã cân nhắc) — Liệt kê những lựa chọn khác và lý do không chọn chúng. Cực kỳ giá trị: nó ngăn người sau đặt lại đúng câu hỏi bạn đã trả lời ("Sao không dùng MongoDB?" → "Đã cân nhắc ở ADR-012, mục Alternatives").
  • Decision drivers (Tiêu chí quyết định) — Những yếu tố nào có trọng số cao nhất khi chọn (chi phí, độ trễ, năng lực team...).
  • Date & Deciders (Ngày và người quyết định) — Ai chịu trách nhiệm, khi nào.
Một biến thể phổ biến khác là MADR (Markdown Architectural Decision Records), một template chuẩn hóa trên GitHub, tổ chức các trường trên một cách rõ ràng hơn. Nhưng bản chất vẫn xoay quanh bộ ba Context–Decision–Consequences.

Tình huống thực tế

Ví dụ 1 — Sàn TMĐT giả định "ChoTot-style" chọn giữa SQL và NoSQL

Một công ty thương mại điện tử ở Hà Nội (ta gọi là MuaNhanh) đang xây dịch vụ giỏ hàng. Team tranh cãi giữa PostgreSQL và DynamoDB suốt hai buổi họp. Cuối cùng họ viết ADR-023:

  • Context: Giỏ hàng có lượng ghi rất cao vào giờ flash sale (đỉnh ~8.000 ghi/giây lúc 12h trưa), dữ liệu đơn giản (key–value theo user_id), và team có 6 người, tất cả đều thạo PostgreSQL nhưng chưa ai từng vận hành DynamoDB trên production.
  • Decision: "Chúng tôi dùng PostgreSQL với bảng cart, kèm Redis làm cache ghi, và sẽ đánh giá lại khi vượt 15.000 ghi/giây."
  • Consequences: Tích cực — team làm chủ ngay, không chi phí học tập; tận dụng được backup, monitoring sẵn có. Tiêu cực — sẽ phải tự lo scaling khi vượt ngưỡng; có nợ kỹ thuật cần theo dõi. Trung tính — thêm phụ thuộc Redis.
Bài học: ADR này không nói PostgreSQL "tốt hơn" DynamoDB. Nó nói với bối cảnh của MuaNhanh lúc này — team nhỏ, deadline gấp, năng lực nghiêng về SQL — thì PostgreSQL là lựa chọn đúng. Chín tháng sau, khi lượng ghi thật sự vượt ngưỡng, người kế nhiệm đọc lại ADR-023, thấy ngay điều kiện "đánh giá lại khi vượt 15.000 ghi/giây" đã ghi sẵn, và bắt tay migrate một cách có kế hoạch thay vì hoảng loạn. Trường Consequences (mục tiêu cực) đã làm đúng nhiệm vụ cảnh báo tương lai.

Ví dụ 2 — Startup logistics và bài học về ADR bị thay thế (Superseded)

Một startup giao vận ở TP.HCM (gọi là GiaoToc) ban đầu viết ADR-005: "Dùng một monolith PHP cho toàn bộ nghiệp vụ". Bối cảnh lúc đó: 4 kỹ sư, chưa tới 500 đơn/ngày, cần ra mắt trong 6 tuần. Quyết định monolith hoàn toàn hợp lý.

Hai năm sau, công ty đã có 40 kỹ sư và 200.000 đơn/ngày. Họ không xóa hay sửa ADR-005. Thay vào đó họ viết ADR-041: "Tách dịch vụ định tuyến giao hàng ra khỏi monolith", và cập nhật trạng thái ADR-005 thành "Superseded by ADR-041" cho phần liên quan.

Bài học: Đây là minh họa sống động cho tính bất biến của ADR. ADR-005 không "sai" — nó đúng với bối cảnh của nó. Khi một nhân viên mới năm thứ ba đọc lịch sử ADR, họ thấy cả một câu chuyện: vì sao khởi đầu bằng monolith (ADR-005), vì sao và khi nào bắt đầu tách (ADR-041). Chuỗi ADR trở thành sách giáo khoa nội bộ về cách hệ thống tiến hóa. Nếu GiaoToc đã xóa ADR-005 khi tách service, họ sẽ mất luôn ký ức "vì sao lúc đầu lại chọn monolith" — và rất có thể một kỹ sư nào đó sẽ phán xét sai lầm những người đi trước.

Ví dụ 3 — Grab và giá trị của trường "Alternatives"

Xét một tình huống ở tầm khu vực Đông Nam Á. Một team tại một công ty siêu ứng dụng (lấy cảm hứng từ Grab) cần chọn công nghệ hàng đợi tin nhắn cho hệ thống điều phối tài xế. Họ viết ADR với một trường Alternatives rất kỹ:

  • Kafka — chọn, vì cần throughput cao và replay được sự kiện.
  • RabbitMQ — loại, vì mô hình routing linh hoạt không phải nhu cầu chính, và họ cần lưu trữ log sự kiện lâu dài.
  • AWS SQS — loại, vì lúc đó cần kiểm soát chi tiết partition và ordering theo khu vực địa lý.
Bài học: Sáu tháng sau, một team khác trong công ty gặp bài toán tương tự và định đề xuất RabbitMQ. Trước khi mở cuộc họp, họ tìm thấy ADR này, đọc mục Alternatives, và hiểu ngay tại sao RabbitMQ đã bị loại trong bối cảnh điều phối tài xế. Họ tiết kiệm được cả tuần tranh luận. Trường Alternatives chính là "vắc-xin chống lại việc lặp lại tranh cãi". (Việc phân tích trade-off giữa các phương án này sâu tới đâu sẽ được bàn ở Bài 6; ở đây trọng tâm là chỗ để ghi chúng trong cấu trúc ADR.)

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

Dưới đây là quy trình viết một ADR đúng cấu trúc, áp dụng được ngay:

Bước 1 — Đặt tên file và số thứ tự. Quy ước phổ biến: adr/0012-su-dung-postgresql-cho-don-hang.md. Đánh số bốn chữ số để sắp xếp đẹp. Lưu ngay trong repo, cạnh code (thư mục docs/adr/ hoặc adr/), để ADR được version control cùng codebase.

Bước 2 — Viết Title. Một cụm danh từ mô tả quyết định. Tránh tiêu đề mơ hồ như "Vấn đề database". Hãy viết "Chọn PostgreSQL làm CSDL chính cho dịch vụ đơn hàng".

Bước 3 — Đặt Status ban đầu là "Proposed". Chưa vội để "Accepted". Trạng thái này báo cho mọi người biết ADR đang chờ thảo luận.

Bước 4 — Viết Context. Đây là phần dành nhiều thời gian nhất. Trả lời: Vấn đề nghiệp vụ là gì? Ràng buộc kỹ thuật nào đang tồn tại? Năng lực team ra sao? Có deadline, ngân sách, hay quy định pháp lý nào không? Viết trung lập, mô tả lực đẩy, chưa nói giải pháp. Mẹo: một người ngoài team đọc Context phải hiểu được tình huống mà không cần hỏi thêm.

Bước 5 — (Khuyến khích) Liệt kê Alternatives. Ghi 2–4 phương án đã cân nhắc và một dòng lý do vì sao mỗi phương án được chọn hoặc bị loại.

Bước 6 — Viết Decision. Một câu chủ động, dứt khoát: "Chúng tôi sẽ...". Nếu có điều kiện đi kèm (như ngưỡng đánh giá lại), ghi luôn ở đây.

Bước 7 — Viết Consequences. Chia rõ ba nhóm: tích cực, tiêu cực, trung tính. Bắt buộc phải có ít nhất một hệ quả tiêu cực — nếu bạn không tìm ra hệ quả xấu nào, khả năng cao là bạn chưa suy nghĩ đủ sâu.

Bước 8 — Review và chuyển Status thành "Accepted". Đưa ADR ra thảo luận (thường qua pull request để cả team comment). Khi đồng thuận, đổi trạng thái và merge.

Bước 9 — Khi quyết định thay đổi về sau, đừng sửa ADR cũ. Viết ADR mới, và cập nhật ADR cũ thành "Superseded by ADR-XXX".

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

Lỗi 1 — Viết Context nghèo nàn. Đây là lỗi phổ biến nhất. Nhiều người nhảy thẳng vào "chúng tôi chọn Kafka" mà không giải thích bối cảnh. Ba năm sau, "chọn Kafka" trở nên vô nghĩa vì không ai biết ràng buộc lúc đó. Mẹo: dành 60% công sức cho Context. Nếu người đọc tương lai chỉ có thể đọc một phần, hãy để đó là Context.

Lỗi 2 — Consequences chỉ toàn màu hồng. ADR biến thành tờ rơi quảng cáo cho công nghệ được chọn. Mẹo: ép mình viết ít nhất một hệ quả tiêu cực và một nợ kỹ thuật cần theo dõi. Sự trung thực ở đây là dấu hiệu của người viết trưởng thành.

Lỗi 3 — Sửa ADR đã Accepted. Điều này phá vỡ tính bất biến và làm mất lịch sử tư duy. Mẹo: coi ADR như một commit trong git — đã ghi thì không viết lại, chỉ thêm cái mới đè lên bằng "Superseded".

Lỗi 4 — Viết ADR quá dài. Một ADR 15 trang thì không ai đọc. Nygard cố tình giữ nó ngắn có lý do. Mẹo: mục tiêu một trang, đọc dưới 5 phút. Nếu dài hơn, có thể bạn đang gộp nhiều quyết định — hãy tách thành nhiều ADR.

Lỗi 5 — Để ADR nằm ngoài repo (trên Confluence, Google Docs rời rạc). ADR dễ bị lạc và không đi cùng phiên bản code. Mẹo: lưu ADR dạng Markdown ngay trong repo, để nó được review qua pull request và version cùng code.

Mẹo tổng quát: Bắt đầu nhỏ. Đừng cố viết ADR cho mọi thứ ngay. Chọn 1–2 quyết định lớn gần đây, viết thử theo đúng năm trường. Khi cả team quen với nhịp này, nó trở thành phản xạ tự nhiên.

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

Bài tập 1 — Viết ADR đầu tiên. Chọn một quyết định kỹ thuật thật mà bạn hoặc team đã ra trong 3 tháng qua (chọn ngôn ngữ, framework, database, thư viện...). Viết một ADR đầy đủ theo năm trường: Title, Status, Context, Decision, Consequences. Ép mình viết ít nhất hai hệ quả tiêu cực trong phần Consequences. Đặt tên file theo quy ước 0001-mo-ta-ngan.md.

Bài tập 2 — "Khảo cổ" một quyết định cũ. Tìm trong hệ thống hiện tại của bạn một quyết định kiến trúc mà không ai còn nhớ rõ lý do. Phỏng vấn người liên quan (hoặc đọc lịch sử git, tin nhắn cũ) và viết ngược lại một ADR cho nó. Chú ý phần Context — bạn sẽ thấy việc tái dựng bối cảnh khó thế nào, và hiểu vì sao ghi lại ngay tại thời điểm quyết định lại quan trọng.

Bài tập 3 — Thực hành "Superseded". Lấy ADR ở Bài tập 1. Giả định 18 tháng đã trôi qua và bối cảnh thay đổi (lưu lượng tăng 10 lần, hoặc team mở rộng gấp đôi). Viết một ADR thứ hai thay thế quyết định cũ, và cập nhật trạng thái ADR đầu tiên thành "Superseded by ADR-0002". Đọc lại cả hai như một chuỗi — bạn có thấy nó kể được một câu chuyện tiến hóa không?

Tóm tắt

ADR — Architecture Decision Record, theo định nghĩa của Michael Nygard năm 2011 — là một tài liệu ngắn ghi lại một quyết định kiến trúc quan trọng cùng bối cảnh và hệ quả của nó. Nó tồn tại để chữa căn bệnh "kiến thức bốc hơi" khi con người rời đi và ký ức phai nhạt.

Những điểm cốt lõi cần nhớ về cấu trúc:

  • Bộ khung tối thiểu gồm năm trường: Title, Status, Context, Decision, Consequences. Có thể bổ sung Alternatives, Decision drivers, Date & Deciders.
  • Context là linh hồn — viết trung lập, mô tả các lực đẩy và ràng buộc, dành nhiều công sức nhất cho nó.
  • Consequences phải thành thật, ghi cả mặt tiêu cực; đây là phần cứu đội ngũ tương lai khỏi bất ngờ.
  • ADR là bất biến: đã Accepted thì không sửa, chỉ thay thế bằng ADR mới với trạng thái "Superseded".
  • Giữ ADR ngắn (một trang), lưu dạng Markdown ngay trong repo để version cùng code.
Khi bạn biến việc viết ADR thành phản xạ, bạn không chỉ tạo ra tài liệu — bạn đang xây một dòng thời gian tư duy cho cả hệ thống, món quà quý giá nhất bạn để lại cho những người kế nhiệm. Ở Bài 5, chúng ta sẽ trả lời câu hỏi tiếp theo một cách rạch ròi: khi nào thì một quyết định đủ lớn để xứng đáng có một ADR — tức là ngưỡng (threshold) quyết định.