Năm view để giải thích cùng một hệ thống
Câu hỏi bài này trả lời: cần sơ đồ nào để trả lời câu hỏi về người dùng, cấu trúc chạy, trình tự xử lý, nơi triển khai và quan hệ dữ liệu mà không trộn các mức chi tiết?
Cần biết trước: layer/module/aggregate, HTTP và transaction. Case tiếp tục ứng dụng đặt hàng của bài đó: hai module ordering/inventory trong một ứng dụng, không tách mỗi aggregate thành service. Đây là thiết kế minh họa; chưa triển khai hoặc kiểm tải/HA.
Chốt một mô hình trước khi vẽ
OrderSystem phục vụ khách đặt/hủy đơn và nhân viên cập nhật tồn. Không có thanh toán, email hoặc đối tác ngoài phạm vi case. BrowserUI là mã HTML/JS của hệ thống chạy trong trình duyệt; OrderApp là ứng dụng Python chứa HTTP, use case, domain và adapter; StoreSQL là PostgreSQL giữ hai schema ordering/inventory.
Order và các dòng giữ quy tắc trạng thái/quantity, StockItem giữ 0 <= reserved <= on_hand. Đặt hàng ở case này dùng một transaction SQL cục bộ để lưu đơn và reservation. Cần hai schema owner và hợp đồng gọi module rõ dù chung database; chọn transaction này không tự cho quyền gọi vào chi tiết domain/adapter của module khác.
| View | Câu hỏi chính | Điều chủ động bỏ bớt |
|---|---|---|
| Context | Ai dùng hệ thống và dùng để làm gì? | Framework, bảng, node triển khai |
| Container | Ứng dụng/kho dữ liệu nào chạy và nói chuyện qua gì? | Số replica, cấu trúc từng class |
| Sequence | Một request diễn ra theo thứ tự nào, lỗi dừng ở đâu? | Mọi use case và mọi query |
| Deployment | Instance nằm ở đâu trong một môi trường cụ thể? | Kiến trúc nghiệp vụ thay thế |
| ERD | Bản số và khóa nào ràng buộc dữ liệu? | Thời gian/giao thức mạng |
Các sơ đồ dùng Mermaid cùng legend, giữ một source inline mỗi view. C4 không bắt buộc một ký pháp; điều quan trọng là cấp zoom và nhãn đọc được. Trang vẽ sơ đồ trực tiếp; khi JavaScript không tải được, mã sơ đồ vẫn còn để đọc.
1. Context: ai tương tác với OrderSystem?
Legend: [person] là vai trò người; [system] là phần mềm trong phạm vi. --label--> chỉ hướng người chủ động yêu cầu hành vi; phản hồi có nhưng không phải một hệ thống mới.
flowchart TB
Customer["Customer · person"] -->|place/cancel order| OrderSystem["OrderSystem · system<br/>Order and inventory behavior"]
Operator["Operator · person"] -->|adjust stock| OrderSystem
Customer không gọi trực tiếp database. Operator là vai trò khác, không mặc định được hủy đơn của khách; quyền thao tác phải được thiết kế và kiểm ở ứng dụng. Context cho thấy ranh giới trách nhiệm, không chứng minh cơ chế authorization đã chạy.
Không đặt Orders table hoặc OrderAggregate vào view này. Khi thêm cổng thanh toán thật, bổ sung hệ thống ngoài và quan hệ cụ thể; đừng vẽ sẵn một đối tác chưa có yêu cầu.
2. Container: bên trong có gì chạy?
Legend: [app] là ứng dụng chạy, [data] là kho dữ liệu. Cạnh có hành động và giao thức; các module trong cùng process dùng contract/in-process call, không phải HTTP hop.
flowchart TB
Customer["Customer · person"] -->|interact| BrowserUI
Operator["Operator · person"] -->|interact| BrowserUI
subgraph OrderSystem["OrderSystem boundary"]
BrowserUI["BrowserUI · app · HTML/JS<br/>User device · untrusted input"]
OrderApp["OrderApp · app · Python<br/>ordering + inventory · contract calls"]
StoreSQL[("StoreSQL · data · PostgreSQL<br/>ordering/inventory schemas")]
BrowserUI -->|submit/read · HTTPS/JSON| OrderApp
OrderApp -->|load/save · SQL/TLS| StoreSQL
end
“Container” ở đây là khái niệm C4 cho ứng dụng/kho dữ liệu, không đồng nghĩa Docker container. BrowserUI là client của hệ thống nhưng môi trường thực thi thuộc thiết bị người dùng: không tin giá, status hay quyền do client tự gửi.
Ordering sở hữu orders/order_items/outbox, inventory sở hữu stock_items/reservations. OrderApp gọi hợp đồng inventory để giữ/trả tồn; module không đọc/ghi tùy ý vào schema bên kia. Outbox là bảng, không tự trở thành một broker hay container mới. EdgeProxy là hạ tầng triển khai, được làm rõ ở view 4.
3. Sequence: đặt đơn, commit rồi mới trả thành công
Legend: số là thứ tự; mũi tên liền là lời gọi, mũi tên đứt là phản hồi. [internal] chỉ việc bên trong cùng OrderApp. Khối alt tách các kết quả; không phải hai nhánh đều chạy.
sequenceDiagram
actor Customer
participant BrowserUI
participant OrderApp
participant StoreSQL
Customer->>BrowserUI: 01 submit
BrowserUI->>OrderApp: 02 POST [HTTPS]
OrderApp->>OrderApp: 03 validate/auth [internal]
OrderApp->>StoreSQL: 04 BEGIN [SQL/TLS]
OrderApp->>StoreSQL: 05 find request_key
StoreSQL-->>OrderApp: 06 existing/absent
alt existing committed request
OrderApp->>StoreSQL: end read transaction
OrderApp-->>BrowserUI: return saved result, no new reservation
else new request
OrderApp->>OrderApp: 07 reserve stock [internal]
OrderApp->>StoreSQL: 08 lock/check/update stock
StoreSQL-->>OrderApp: 09 enough/missing
alt missing stock
OrderApp->>StoreSQL: 10 ROLLBACK
OrderApp-->>BrowserUI: 11 409 failure
else enough stock
OrderApp->>OrderApp: 12 create Order [internal]
OrderApp->>StoreSQL: 13 save order/items
OrderApp->>StoreSQL: 14 save reservations
OrderApp->>StoreSQL: 15 COMMIT
StoreSQL-->>OrderApp: 16 commit confirmed
OrderApp-->>BrowserUI: 17 201 order
BrowserUI-->>Customer: 18 show result
end
end
Ở bước 3, server kiểm quyền và dữ liệu, tính lại giá theo nguồn đáng tin của case; không dùng total từ BrowserUI như bằng chứng giá đúng. Khi giữ nhiều StockItem, cần thứ tự lấy khóa nhất quán; xem deadlock.
Bước 5 tìm yêu cầu đã commit bằng request_key duy nhất. Nhánh đã có kết quả kết thúc transaction đọc trước khi trả kết quả cũ. Hai request trùng có thể cùng thấy absent: UNIQUE ở orders và xử lý xung đột/retry vẫn cần thiết; mũi tên “find” không tự cung cấp exactly-once.
Nhánh thiếu tồn rollback cả các cập nhật trước đó, trả thất bại; không để Order CONFIRMED nếu reservation chưa có. COMMIT xong mới trả 201. Nếu phản hồi mất sau commit, gửi lại cùng request_key để lấy kết quả đã lưu; timeout mạng tự nó không cho biết commit có xảy ra hay không.
Hủy đơn là use case khác: Order đổi trạng thái và ghi OrderCancelled vào outbox cùng commit; phần xử lý event trong OrderApp trả reservation qua contract inventory, có chống trùng. View này không vẽ toàn luồng hủy hoặc mô tả outbox như bảo đảm tự có: cần thiết kế retry/persistence cho luồng đó.
4. Deployment: ánh xạ instance vào staging minh họa
Legend: tên kết thúc #1 là một instance; các zone mô tả trust boundary. EdgeProxy kết thúc TLS; hop HTTP tới app chỉ ở mạng private đã giới hạn. Số instance là giả định staging, không phải thông số production đã đo.
flowchart TB
subgraph Device["User device · untrusted"]
BrowserUI["BrowserUI#1"]
end
subgraph Edge["Public edge"]
EdgeProxy["EdgeProxy#1<br/>Reverse proxy · TLS termination"]
end
subgraph Application["Private application zone"]
OrderApp["OrderApp#1 · Python process<br/>ordering + inventory"]
end
subgraph Data["Private data zone"]
StoreSQL[("StoreSQL#1 · PostgreSQL<br/>ordering/inventory schemas")]
end
BrowserUI -->|submit/read · HTTPS/JSON · Internet boundary| EdgeProxy
EdgeProxy -->|forward · HTTP/JSON · private app ingress| OrderApp
OrderApp -->|load/save · SQL/TLS · database credential| StoreSQL
Môi trường staging minh họa có một app và một database; không có public database ingress, replica hoặc failover.
Container view nói BrowserUI gọi OrderApp qua tuyến HTTPS của hệ thống; deployment bổ sung hop EdgeProxy và vị trí kết thúc TLS. SQL/TLS nhất quán ở hai view. Nếu mạng nội bộ chưa đáp ứng chính sách tin cậy, cần TLS/mTLS cho hop app; nhãn “private” không tự bảo đảm mạng an toàn.
EdgeProxy chỉ được forward vào cổng app đã cho phép; StoreSQL chỉ nhận từ identity/network của OrderApp theo chính sách triển khai. Những dòng này là yêu cầu thiết kế, chưa có firewall/credential/HA được dựng. Một app và một database là điểm lỗi đơn; không gắn nhãn “high availability” cho sơ đồ này.
5. ERD: quan hệ dữ liệu và ownership
Legend: 1 -> 0..N nghĩa mỗi hàng bên phải thuộc đúng một hàng trái, phía trái có thể chưa có hàng con. ref chỉ tham chiếu ID qua biên module, không là FK được enforce ở ví dụ. Các FK được nêu tường minh bên dưới; ERD không phải aggregate map.
erDiagram
direction TB
orders ||--o{ order_items : "FK order_id"
orders ||--o{ outbox : "FK order_id"
stock_items ||--o{ reservations : "FK product_id"
stock_items ||..o{ order_items : "ref product_id · application only"
orders ||..o{ reservations : "ref order_id · application only"
orders {
bigint id PK
string request_key UK
bigint customer_id
string status
}
order_items {
bigint order_id PK,FK
int line_no PK
bigint product_id
}
outbox {
string event_id PK
bigint order_id FK
string event_type
json payload
}
stock_items {
bigint product_id PK
int on_hand
int reserved
}
reservations {
bigint order_id PK
bigint product_id PK,FK
int quantity
}
orders, order_items và outbox thuộc schema ordering; stock_items và reservations thuộc schema inventory. Cạnh liền ghi FK; cạnh đứt ghi ref do ứng dụng bảo vệ, không có FK xuyên schema trong case này.
Orders có thể DRAFT chưa có dòng, nên ERD dùng 0..N; hành vi confirm yêu cầu ít nhất một dòng. FK chỉ ràng buộc tham chiếu, không ràng buộc “đơn confirmed phải có dòng”. Quantity của dòng/reservation dương, StockItem có 0 <= reserved <= on_hand; phải thiết kế enforcement trong domain và persistence phù hợp.
Hai ref không được vẽ thành FK âm thầm: case chọn tránh FK xuyên schema owner, đổi lại ứng dụng và kiểm đối soát phải bảo vệ tham chiếu/cleanup. Trong một monolith dùng FK xuyên schema cũng có thể hợp lý, nhưng đó là quyết định khác cần nói rõ chi phí coupling.
Một order có nhiều reservation theo sản phẩm, mỗi reservation giữ một product_id và một order_id. Tổng reserved và số reservation active phải nhất quán theo chính sách inventory; ERD chưa mô tả status/release history, index chi tiết hay migration SQL. Không dùng sơ đồ rút gọn này để tạo schema production trực tiếp.
Khi đã có DDL thật, có thể dán vào sql2erd.dev để xem ERD trong trình duyệt và xuất Mermaid hoặc ảnh; công cụ này là sản phẩm khác của tác giả, bài không cần đến nó. Vẫn đối chiếu sơ đồ sinh ra với legend ở trên: FK do DDL enforce khác ref do ứng dụng bảo vệ, nên kiểm xem công cụ có phân biệt hai loại cạnh này không trước khi dùng sơ đồ để review.
Đối chiếu view trước khi review
| Điều cần đối chiếu | Kết quả của case | Dấu hiệu lệch cần sửa |
|---|---|---|
| Actor | Customer/Operator có cùng vai trò ở context/container | Một view tự thêm admin hoặc đối tác |
| Runtime | ordering/inventory cùng OrderApp | Sequence vẽ HTTP giữa hai module |
| Protocol | HTTPS ở tuyến client, HTTP private sau edge, SQL/TLS tới StoreSQL | Deployment biến SQL thành REST hoặc thiếu hop TLS |
| Transaction | Reservation và Order cùng commit đặt hàng | Trả 201 trước commit hoặc nhánh lỗi vẫn commit |
| Storage | Năm bảng ở StoreSQL, ownership schema rõ | Outbox bỗng thành Kafka hay database ngoài |
| Cardinality | ERD cho DRAFT không dòng, confirm có invariant riêng | Vẽ bắt buộc 1..N rồi ví dụ có DRAFT rỗng |
| Trust | Client untrusted, data zone không public | Container có BrowserUI nối SQL trực tiếp |
Một bộ view nhất quán vẫn có thể mô tả thiết kế sai. Review tiếp bằng yêu cầu: khi mất response/DB/instance thì hành vi nào cần giữ, ai có quyền sửa trạng thái, dữ liệu nào phải phục hồi và phép kiểm nào chứng minh được. Các sơ đồ không thay capacity planning, threat modeling hoặc test tích hợp.
Bài tập và nguồn
- Thêm thanh toán ngoài hệ thống: cập nhật context trước, rồi container/sequence/deployment; không chỉ thêm một mũi tên ở sequence.
- Tách inventory thành process riêng: transaction cục bộ của case không còn đủ; viết lại protocol và hành vi thất bại, không chỉ đổi tên module thành service.
- Thêm replica đọc: chỉ rõ query nào chấp nhận stale data, rồi kiểm sequence read-after-write; hình deployment không chứng minh dữ liệu mới xuất hiện ngay.
Nguồn sơ cấp đọc ngày 2026-10-03; các lựa chọn transaction/schema/deployment là thiết kế của ví dụ:
- C4, System context, Container và Deployment: câu hỏi và phạm vi từng mức.
- C4, Notation: nhãn, loại phần tử và legend; ký pháp không bị giới hạn ở một công cụ.
- Tổ chức code theo layer/module/aggregate: domain ownership của case và giới hạn bảo vệ bằng folder.