Design doc: giải thích một thay đổi trước khi viết code
Câu hỏi: người review cần biết điều gì để tìm ra một quyết định sai, thay vì chỉ kiểm tài liệu đủ mục?
Cần biết trước: yêu cầu chức năng, transaction, ranh giới module. Bài dùng cùng ví dụ đơn hàng của tổ chức code và ADR. Khung và ví dụ dưới đây là tài liệu thiết kế, không phải ứng dụng hay kết quả benchmark đã chạy.
Chọn độ dài theo câu hỏi cần quyết
Design doc đề xuất cách giải quyết một thay đổi cùng lựa chọn, rủi ro và cách kiểm. Tài liệu kiến trúc mô tả hệ thống hiện tại; ADR giữ lý do của một quyết định. Có thể đưa một quyết định từ design doc sang ADR sau khi chốt, rồi cập nhật kiến trúc khi triển khai xong. Ba vai trò này không bắt buộc thành ba bộ tài liệu dài.
Design Docs at Google là mô tả thực hành của tác giả, có đề xuất cách trình bày giải pháp và các phương án khác; nó không đặt một chuẩn bắt buộc cho mọi đội. arc42 nhấn mạnh mục tiêu, yêu cầu và người liên quan. C4 cung cấp các mức view khác nhau; chỉ thêm sơ đồ khi nó trả lời một câu hỏi người review đang có.
Với thay đổi nhỏ trong một module, thử một brief khoảng hai trang: một trang giải thích vấn đề, lựa chọn và hợp đồng; một trang nói lỗi, triển khai và cách nghiệm thu. “Hai trang” là giới hạn biên tập gợi ý, không tiêu chuẩn. Nếu cần đồng bộ nhiều đội, chuyển dữ liệu lâu dài hoặc thay biên quyền truy cập, tách phần chi tiết để review rõ; đừng ép một thiết kế chưa hiểu vào hai trang.
Khung tối thiểu để điền
Viết mỗi phần đủ để một người chưa tham gia cuộc trao đổi có thể kiểm hoặc phản bác.
| Phần | Câu hỏi phải trả lời |
|---|---|
| Vấn đề và kết quả | Ai đang gặp việc gì? Đầu vào/kết quả nào thay đổi? Kết quả nào quan sát được? |
| Phạm vi và ràng buộc | Module nào sở hữu? Điều gì phải giữ? Phần nào chưa xử lý? |
| Phương án và quyết định | Các lựa chọn khả thi là gì? Ràng buộc nào khiến chọn một cách? |
| Hợp đồng và luồng lỗi | API/dữ liệu nào đổi? Concurrent call, retry và lỗi giữa chừng xử lý thế nào? |
| Triển khai và kiểm | Chia bước ra sao? Test nào đủ để nhận? Khi nào dừng hoặc phục hồi? |
| Điều chưa biết và người chốt | Giả định nào cần xác minh? Ai quyết định trước khi triển khai? |
Không điền tên công nghệ chỉ để có mục “scalability”. Nếu chưa có workload, ghi dữ kiện cần đo trước khi chọn cache hoặc thêm replica. Đừng viết “nhanh, bảo mật, dễ mở rộng” thành yêu cầu mà không có kịch bản kiểm.
Ví dụ điền sẵn: thêm lý do hủy đơn
Trạng thái: Proposed trong case giả. Người quyết: chủ quy tắc đơn hàng và người phụ trách tồn kho; chưa có phê duyệt một hệ thống thật.
Vấn đề và phạm vi
Hiện yêu cầu hủy không giữ lý do. Người xử lý đơn cần biết lý do đầu tiên đã được chấp nhận, kể cả khi client gửi lại yêu cầu sau lỗi mạng. Thành công là một đơn CONFIRMED chuyển CANCELLED với lý do đã trim, một sự kiện hủy và một lần trả tồn. SHIPPED phải bị từ chối. Gửi lại một đơn đã CANCELLED không ghi đè lý do đầu.
Ordering sở hữu trạng thái/lý do hủy; inventory sở hữu reservation và
0 <= reserved <= on_hand. Một ứng dụng và một database là ràng buộc của case.
Không thêm service, broker hay notification trong thay đổi này. Chưa định nghĩa
giới hạn độ dài lý do hoặc chính sách lưu dữ liệu cá nhân: cần người sở hữu chốt
trước khi mở input thật, không tự chọn một con số rồi gọi là yêu cầu.
Các cách làm và lựa chọn
| Lựa chọn | Lợi ích trong case | Chi phí và lỗi cần xét |
|---|---|---|
| Controller cập nhật trực tiếp status và reason | Ít đường gọi lúc đầu | Caller khác có thể bỏ qua quy tắc SHIPPED/hủy lặp; inventory ownership không rõ |
| CancelOrder gọi Order, lưu qua OrderStore, gọi hợp đồng inventory | Giữ quy tắc ở owner; adapter giữ SQL/concurrency | Cần wiring và hợp đồng transaction/version cụ thể |
| Tách dịch vụ hủy với database riêng | Có biên deploy riêng | Thêm lỗi liên dịch vụ và nhất quán dữ liệu khi hiện chưa cần deploy riêng |
Đề xuất cách thứ hai. Nó nối tiếp quyết định module ngoài/layer trong của ADR-0001, không chọn vì nhiều class hơn. Chi phí chấp nhận là phải kiểm cả domain lẫn adapter; một method Order đúng trong RAM không chặn hai request cùng đọc CONFIRMED.
Hợp đồng và transaction đề xuất
API minh họa: POST /orders/{order_id}/cancel, body {"reason":"duplicate order"}.
Xác thực và quyền hủy đã là điều kiện của entry point, không suy từ việc biết order_id.
Domain nhận reason đã được kiểm input, tự kiểm trạng thái và giữ lý do đầu.
Application nạp Order qua port, áp dụng hành vi và lưu trong transaction đã chọn.
Adapter dùng version check hoặc khóa hàng để hai caller không cùng commit việc hủy.
Trong case một database, thay đổi Order, sự kiện hủy bền và release reservation
cần commit cùng nhau; nếu một phần lỗi, rollback cả transaction. Inventory kiểm
reservation đã release để lời gọi lặp không giảm reserved lần nữa.
Đây là đề xuất hợp đồng bổ sung cho case, chưa có implementation chứng minh việc chia sẻ transaction giữa adapter/module. Trước code, phải chốt ai sở hữu Unit of Work, version conflict được retry ở đâu và event chỉ lưu cục bộ hay được gửi ra ngoài. Nếu gửi event tới hệ thống khác, transaction database không bao phủ mạng: cần một thiết kế delivery/idempotency riêng, không gắn nhãn exactly-once cho method này.
Phản hồi minh họa: lần hủy đầu trả 200 cùng CANCELLED/lý do đã lưu; hủy lặp trả cùng trạng thái đã lưu; SHIPPED hoặc xung đột chưa giải quyết trả 409. Đơn không tồn tại và caller không được phép cần quy tắc trả lỗi phù hợp chính sách quyền; không dùng phản hồi để lộ dữ liệu mà caller không được đọc. HTTP mapping này là lựa chọn của brief, không phải kết quả đo ở bài sequence đặt hàng trả 201/409.
Nghiệm thu bằng điều quan sát được
| Kịch bản | Kết quả phải kiểm trong ứng dụng thật |
|---|---|
| CONFIRMED, reason có khoảng trắng hai đầu | CANCELLED; reason trim; một event; reservation release một lần |
| SHIPPED hoặc reason chỉ có khoảng trắng | Từ chối; state, event và reservation không đổi |
| Gửi lại CANCELLED với reason khác | Giữ reason đầu; không event mới; không trả tồn lần hai |
| Hai caller cùng đọc version cũ | Chỉ một commit hủy; caller còn lại thấy conflict hoặc trạng thái đã hủy |
| Lỗi persistence sau xử lý tồn | Transaction rollback; không có trạng thái hủy nửa chừng |
| Caller không có quyền hoặc thuộc tenant khác | Không đọc/sửa đơn; không phát event, không thay tồn |
Domain test bao phủ trạng thái; integration test với database thật mới kiểm concurrency, transaction và tenant access. Fault injection cần đặt vào đúng ranh giới lưu, rồi đọc lại dữ liệu. Các dòng này chưa được chạy; bài không có ứng dụng để tạo bằng chứng runtime.
Đưa vào vận hành và câu hỏi còn mở
Thêm cột nullable trước, kiểm app cũ còn đọc được, triển khai app mới sau khi contract được chốt và kiểm. Chọn chính sách reason cho đơn đã hủy trước đây; không bịa giá trị lịch sử. Rollback app có thể giữ cột mới; xóa cột làm mất dữ liệu nên là bước riêng.
Theo dõi lỗi hủy, version conflict và reservation release trùng. Chọn ngưỡng từ baseline thật trước khi mở cho mọi caller. Đây là kế hoạch kiểm, chưa có latency, throughput, mức giảm bug hoặc lời hứa zero downtime.
Cần chốt trước triển khai: giới hạn và retention của reason; mapping lỗi/khả năng retry; quyền sở hữu transaction; chính sách cho dữ liệu cũ; định nghĩa event delivery. Brief đang Proposed cho tới khi người có quyền quyết và ghi bằng chứng.
Review tài liệu thay vì đếm mục
Người review thử một câu hỏi ở từng ranh giới: request lặp có đổi kết quả không, hai caller cùng nạp dữ liệu thì ai thắng, mất kết nối sau commit thì làm sao kiểm lại, đổi schema có làm app cũ không đọc được không. Mỗi câu trả lời phải nằm trong hợp đồng hoặc được ghi là câu hỏi mở có người chốt, không chỉ dẫn sang một tài liệu thiếu.
Review xong cần phân biệt: tài liệu nhất quán, thiết kế được chấp nhận, code đúng thiết kế và hệ thống đã nghiệm thu là bốn trạng thái khác nhau. Build Markdown thành công chỉ chứng minh tài liệu render được.
Học tiếp: ADR lưu lý do chọn, năm view của hệ thống, kế hoạch release và deploy. Nguồn công khai đọc ngày 2026-10-13; khung và ví dụ do bài tự thiết kế.