Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

ViewCâu hỏi chínhĐiều chủ động bỏ bớt
ContextAi 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
SequenceMột request diễn ra theo thứ tự nào, lỗi dừng ở đâu?Mọi use case và mọi query
DeploymentInstance nằm ở đâu trong một môi trường cụ thể?Kiến trúc nghiệp vụ thay thế
ERDBả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ếuKết quả của caseDấu hiệu lệch cần sửa
ActorCustomer/Operator có cùng vai trò ở context/containerMột view tự thêm admin hoặc đối tác
Runtimeordering/inventory cùng OrderAppSequence vẽ HTTP giữa hai module
ProtocolHTTPS ở tuyến client, HTTP private sau edge, SQL/TLS tới StoreSQLDeployment biến SQL thành REST hoặc thiếu hop TLS
TransactionReservation và Order cùng commit đặt hàngTrả 201 trước commit hoặc nhánh lỗi vẫn commit
StorageNăm bảng ở StoreSQL, ownership schema rõOutbox bỗng thành Kafka hay database ngoài
CardinalityERD cho DRAFT không dòng, confirm có invariant riêngVẽ bắt buộc 1..N rồi ví dụ có DRAFT rỗng
TrustClient untrusted, data zone không publicContainer 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

  1. 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.
  2. 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.
  3. 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ụ: