·

Tiếng Việt: Domain Modeling And Data Modeling

Domain Modeling And Data Modeling

Nếu bạn để AI vẽ ERD trước khi hiểu domain, bạn sẽ có một schema đẹp nhưng sai — và cái giá của một schema sai chỉ lộ ra sau vài tháng, khi migration đã trở thành ác mộng.

Vì Sao Domain Modeling Phải Đi Trước Data Modeling

Bạn đã bao giờ ngồi trong một buổi review schema mà câu hỏi đầu tiên không phải "cột này kiểu gì" mà là "cái bảng này thực ra đại diện cho cái gì trong nghiệp vụ" chưa? Đó chính xác là lý do domain modeling (mô hình hóa nghiệp vụ) phải đi trước data modeling (mô hình hóa dữ liệu). Data modeling trả lời câu hỏi "lưu trữ thế nào cho hiệu quả". Domain modeling trả lời câu hỏi quan trọng hơn: "cái gì tồn tại trong thế giới nghiệp vụ này, và chúng quan hệ với nhau ra sao".

Vấn đề là AI — dù là Claude Code, Cursor hay ChatGPT — cực kỳ giỏi nhảy thẳng vào bước hai. Bạn ném cho nó một đoạn mô tả tính năng, nó xuất ra ngay một cái CREATE TABLE đầy đủ index, foreign key, constraint. Nhìn rất chuyên nghiệp. Nhưng nếu bạn chưa thống nhất được domain model — chưa biết Order có phải aggregate root (gốc tổng hợp, entity chịu trách nhiệm giữ tính nhất quán cho cả cụm liên quan) hay không, chưa biết Payment là một entity độc lập hay chỉ là value object (đối tượng giá trị, không có định danh riêng) gắn liền với Order — thì cái schema kia chỉ là một bộ đoán có cấu trúc đẹp.

Kinh nghiệm thực tế: một hệ thống giao đồ ăn mà tôi từng review có bảng orders với cột rider_status nằm chung với order_status. Không ai buộc tội AI vì AI sinh ra nó theo đúng yêu cầu "thêm trạng thái tài xế vào order". Vấn đề nằm ở chỗ không ai làm domain modeling trước — không ai đặt câu hỏi "Rider có phải một domain concept độc lập, có vòng đời riêng, có state machine riêng không?". Nếu làm domain modeling trước, câu trả lời sẽ rõ ràng: có, Rider là entity riêng, và mối quan hệ giữa RiderOrder là một quan hệ (association) chứ không phải là gộp field.

Nguyên tắc làm việc với AI ở đây: đừng bao giờ yêu cầu AI "thiết kế database cho tính năng X" ngay lập tức. Hãy tách thành hai bước rõ ràng — bước một là domain modeling (entities, relationships, business rules), bước hai mới là data modeling (bảng, cột, kiểu dữ liệu, index). Nếu bạn gộp hai bước, AI sẽ tối ưu hóa cho tốc độ trả lời chứ không phải cho tính đúng đắn của domain.

Mẹo: Khi bắt đầu một tính năng mới, luôn mở đầu prompt bằng "Trước tiên, đừng thiết kế schema. Hãy liệt kê domain entities và mối quan hệ giữa chúng dựa trên mô tả sau" — câu này buộc AI dừng lại ở tầng khái niệm, tránh nhảy cóc sang SQL.

Xác Định Domain Entities và Relationships Từ Requirements

Requirements thường được viết dưới dạng văn xuôi lộn xộn — một đoạn mô tả tính năng từ product manager, một ticket Jira, hoặc tệ hơn là một cuộc nói chuyện Slack được copy-paste. Việc của bạn, và AI có thể giúp rất tốt ở đây, là "chưng cất" đoạn văn đó thành danh sách entities, thuộc tính, và relationships.

Cách tiếp cận hiệu quả: đưa cho AI đúng nguyên văn đoạn requirements, không diễn giải trước, và yêu cầu nó trích xuất theo một khuôn cố định — danh từ nào là entity, danh từ nào chỉ là thuộc tính, động từ nào ám chỉ một hành vi nghiệp vụ (behavior) cần được model như một method hoặc domain event. Đây là kỹ thuật gần giống linguistic analysis (phân tích ngôn ngữ trong văn bản requirements để tìm danh từ/động từ cốt lõi) trong DDD kinh điển của Eric Evans, chỉ khác là bây giờ bạn có một trợ lý làm việc đó nhanh hơn gấp mười lần.

Ví dụ prompt bạn có thể dùng thẳng:

Đọc đoạn requirements sau và thực hiện phân tích domain:

"Khách hàng đặt món từ một nhà hàng. Mỗi đơn hàng có thể chứa nhiều
món ăn khác nhau, mỗi món có số lượng riêng. Sau khi khách xác nhận,
hệ thống tính tổng tiền, giữ tiền tạm (authorize) qua cổng thanh toán,
rồi tìm một tài xế gần nhà hàng để giao đơn. Nhà hàng có thể từ chối
đơn nếu hết món. Tài xế nhận đơn, lấy hàng, và giao tới khách. Khi
giao xong, hệ thống mới capture tiền thật sự."

Yêu cầu output:
1. Danh sách domain entities (danh từ cốt lõi, có định danh riêng, có vòng đời).
2. Với mỗi entity: các thuộc tính chính, và các trạng thái (state) nó có thể có.
3. Danh sách relationships giữa các entity (một-nhiều, nhiều-nhiều, tùy chọn hay bắt buộc).
4. Danh sách domain events phát sinh trong luồng này (dạng "SomethingHappened").
5. Chỉ ra entity nào có khả năng là Value Object thay vì Entity (không có
   định danh riêng, chỉ được so sánh bằng giá trị).

Với đoạn requirements ở trên, AI thường sẽ trả về đại loại: entities gồm Customer, Restaurant, MenuItem, Order, OrderItem, Rider, Payment; relationship Customer 1-n Order, Restaurant 1-n MenuItem, Order 1-n OrderItem, Order 0..1 Payment, Order 0..1 Rider; domain event như OrderPlaced, PaymentAuthorized, OrderAccepted, RiderAssigned, OrderPickedUp, OrderDelivered, PaymentCaptured. Đây chính là bộ entity mà chúng ta sẽ dùng xuyên suốt các phần sau.

Điểm mấu chốt bạn phải tự kiểm tra thay vì tin tuyệt đối vào AI: liệu OrderItem có thực sự cần là entity riêng, hay chỉ là một dòng dữ liệu gắn chặt vào Order? Câu trả lời phụ thuộc vào việc bạn có cần truy vấn OrderItem độc lập không (ví dụ thống kê món bán chạy) — nếu có, nó nên là entity/bảng riêng; nếu không, nó có thể chỉ là một mảng nhúng. AI không biết ngữ cảnh truy vấn tương lai của bạn, nên phần quyết định này vẫn là của bạn.

Mẹo: Sau khi AI liệt kê entities, hỏi ngược lại nó "Entity nào trong danh sách này có khả năng thực ra là Value Object?" — câu hỏi này thường lôi ra được những nhầm lẫn kinh điển như coi Address hay Money là entity trong khi chúng nên là value object bất biến.

Tạo Entity Relationship Diagram (ERD) Với Mermaid

Một khi đã có danh sách entities và relationships bằng lời, bước tiếp theo là hình ảnh hóa nó. Mermaid là lựa chọn thực dụng nhất hiện nay vì nó render được ngay trong GitHub, GitLab, Notion, và hầu hết công cụ AI code (Claude Code, Cursor) đều viết được cú pháp erDiagram khá chuẩn mà không cần bạn cài thêm plugin nào.

Ví dụ prompt để sinh ERD từ domain đã phân tích ở phần trước:

Dựa trên các entities và relationships đã liệt kê (Customer, Restaurant,
MenuItem, Order, OrderItem, Rider, Payment), hãy sinh một Mermaid
erDiagram đầy đủ. Yêu cầu:
- Ghi rõ cardinality (1-1, 1-n, n-n) bằng ký hiệu Mermaid chuẩn.
- Mỗi entity liệt kê ít nhất 4-5 thuộc tính chính, đánh dấu PK/FK.
- Dùng snake_case cho tên cột, UPPER_SNAKE_CASE cho tên entity.

Và đây là kết quả cụ thể cho domain giao đồ ăn mà chúng ta đang dùng xuyên suốt:

erDiagram
    CUSTOMER ||--o{ ORDER : places
    RESTAURANT ||--o{ MENU_ITEM : offers
    RESTAURANT ||--o{ ORDER : receives
    ORDER ||--|{ ORDER_ITEM : contains
    MENU_ITEM ||--o{ ORDER_ITEM : "referenced in"
    ORDER ||--o| PAYMENT : "paid by"
    RIDER ||--o{ ORDER : delivers

    CUSTOMER {
        uuid id PK
        string full_name
        string email
        string phone
        timestamptz created_at
    }
    RESTAURANT {
        uuid id PK
        string name
        string address
        string cuisine_type
        boolean is_active
    }
    MENU_ITEM {
        uuid id PK
        uuid restaurant_id FK
        string name
        decimal price
        boolean is_available
    }
    ORDER {
        uuid id PK
        uuid customer_id FK
        uuid restaurant_id FK
        uuid rider_id FK
        string status
        decimal total_amount
        timestamptz created_at
    }
    ORDER_ITEM {
        uuid id PK
        uuid order_id FK
        uuid menu_item_id FK
        int quantity
        decimal unit_price
    }
    RIDER {
        uuid id PK
        string full_name
        string phone
        string vehicle_type
        string status
    }
    PAYMENT {
        uuid id PK
        uuid order_id FK
        string method
        string status
        decimal amount
        timestamptz paid_at
    }

Nhìn vào diagram này, bạn nên tự hỏi vài câu phản biện trước khi chấp nhận nó: RIDER có nên biết trực tiếp về ORDER qua khóa ngoại rider_id trong bảng ORDER, hay nên có một bảng trung gian DELIVERY_ASSIGNMENT để lưu lịch sử các lần gán tài xế (kể cả khi tài xế bị đổi giữa chừng)? Với hệ thống thực tế có tỷ lệ đổi tài xế cao, mô hình 1-n qua bảng trung gian đúng hơn. Đây là loại quyết định mà AI sẽ không tự đưa ra trừ khi bạn hỏi thẳng — nó có xu hướng chọn giải pháp đơn giản nhất thỏa yêu cầu hiện tại.

Mẹo: Luôn yêu cầu AI giải thích bằng lời từng cardinality nó chọn ("vì sao Order-Payment là 1-0..1 chứ không phải 1-n") — nếu lời giải thích không khớp với nghiệp vụ thực (ví dụ đơn hàng có thể có nhiều lần thanh toán do retry), bạn bắt lỗi ngay tại bước diagram, rẻ hơn nhiều so với bắt lỗi ở bước migration.

Thiết Kế Schema Với Sự Hỗ Trợ Của AI: Normalization, Indexing, và Data Types

Đây là phần mà nhiều kỹ sư — kể cả senior — vẫn để AI tự quyết định mà không kiểm tra lại, và đây cũng là phần dễ để lại nợ kỹ thuật âm thầm nhất.

Về normalization (chuẩn hóa dữ liệu): mục tiêu không phải là đẩy mọi thứ lên 3NF (Third Normal Form) một cách máy móc, mà là hiểu trade-off. OrderItem lưu unit_price riêng thay vì join sang MenuItem.price là một denormalization có chủ đích — vì giá món ăn có thể thay đổi sau này, nhưng đơn hàng cũ phải giữ nguyên giá tại thời điểm đặt. Đây chính xác là điểm mà một AI thiếu ngữ cảnh sẽ "chuẩn hóa" nhầm, xóa mất cột unit_price vì cho rằng nó "trùng lặp dữ liệu" với MenuItem. Bạn phải chỉ rõ business rule này trong prompt.

Về indexing: đừng để AI đoán mù. Nó cần biết pattern truy vấn thực tế. Một index tốt cho orders là composite index trên (restaurant_id, status, created_at) nếu ứng dụng thường xuyên hỏi "các đơn đang pending của nhà hàng X, sắp theo thời gian" — nhưng AI chỉ đoán được điều này nếu bạn kể cho nó nghe các câu query phổ biến nhất.

Về data types, có ba nguyên tắc kinh điển bạn luôn phải nhắc AI tuân theo, vì mô hình ngôn ngữ có xu hướng chọn kiểu "phổ biến" thay vì kiểu "đúng":

  • Tiền luôn dùng DECIMAL(12,2) hoặc tương đương, không bao giờ dùng FLOAT/DOUBLE — sai số dấu phẩy động sẽ khiến tổng tiền lệch vài xu sau hàng triệu giao dịch.
  • Khóa chính nên cân nhắc UUID khi hệ thống phân tán, cần sinh ID ở client hoặc tránh lộ số lượng bản ghi qua auto-increment; BIGINT auto-increment vẫn tốt hơn cho bảng insert cực nhiều và cần index nhỏ gọn, tối ưu locality.
  • Thời gian luôn dùng kiểu có timezone (TIMESTAMPTZ trong Postgres) chứ không phải TIMESTAMP trần — nếu không, hệ thống giao đồ ăn hoạt động xuyên nhiều múi giờ sẽ tính sai giờ giao hàng ngay lập tức.

Prompt ví dụ để AI sinh DDL có kiểm soát:

Từ ERD ở trên, sinh SQL DDL (Postgres) cho bảng orders, order_items,
payments. Yêu cầu bắt buộc:
- Tiền: DECIMAL(12,2), không dùng FLOAT/DOUBLE.
- Khóa chính: UUID (dùng gen_random_uuid()).
- Thời gian: TIMESTAMPTZ, default now().
- Thêm composite index phục vụ query "lấy đơn pending theo restaurant_id,
  sắp xếp theo created_at giảm dần".
- order_items.unit_price phải lưu độc lập, KHÔNG join sang menu_items.price
  (vì giá món có thể đổi sau khi đơn đã đặt).
- Thêm CHECK constraint cho orders.status chỉ nhận các giá trị hợp lệ.

Output rút gọn AI thường trả về:

CREATE TABLE orders (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    customer_id UUID NOT NULL REFERENCES customers(id),
    restaurant_id UUID NOT NULL REFERENCES restaurants(id),
    rider_id UUID REFERENCES riders(id),
    status TEXT NOT NULL CHECK (status IN
        ('pending','accepted','rejected','picked_up','delivered','cancelled')),
    total_amount DECIMAL(12,2) NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX idx_orders_restaurant_status_created
    ON orders (restaurant_id, status, created_at DESC);

CREATE TABLE order_items (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    order_id UUID NOT NULL REFERENCES orders(id),
    menu_item_id UUID NOT NULL REFERENCES menu_items(id),
    quantity INT NOT NULL CHECK (quantity > 0),
    unit_price DECIMAL(12,2) NOT NULL
);

Bạn thấy đấy — kết quả chỉ tốt khi ràng buộc nghiệp vụ được nói rõ trong prompt. Không nhắc gì, khả năng cao AI sẽ dùng FLOAT cho giá và TIMESTAMP không timezone, trông vẫn chạy được trên máy dev nhưng sẽ nổ ở production.

Mẹo: Giữ một "checklist chuẩn" (money → DECIMAL, id → UUID/BIGINT có lý do, time → TIMESTAMPTZ, enum trạng thái → CHECK constraint hoặc bảng lookup) và dán thẳng checklist này vào cuối mọi prompt sinh schema — đừng trông cậy AI tự nhớ quy ước của bạn giữa các phiên làm việc.

Event Storming Với AI

Event storming (tạm dịch: phiên làm việc dựng bão sự kiện) là kỹ thuật của Alberto Brandolini, nơi cả team dán sticky note màu cam lên tường, mỗi note là một domain event viết ở thì quá khứ ("OrderPlaced", không phải "PlaceOrder"), rồi sắp chúng theo trình tự thời gian để lộ ra toàn bộ luồng nghiệp vụ. Vấn đề kinh điển: tổ chức một buổi event storming trực tiếp cần cả team ngồi lại, tốn nửa ngày, và thường lạc đề.

AI không thay thế được buổi event storming trực tiếp có domain expert tham gia — nhưng nó là công cụ tuyệt vời để chuẩn bị trước (draft nháp để team chỉnh sửa) hoặc để nhanh chóng số hóa kết quả sau buổi họp thành tài liệu sống. Cách dùng hiệu quả: đưa AI mô tả luồng nghiệp vụ, yêu cầu nó liệt kê domain events theo đúng convention (danh động từ ở thì quá khứ), kèm actor (ai kích hoạt), command (hành động gây ra event), và policy (event nào tự động kích hoạt command tiếp theo).

Prompt ví dụ:

Thực hiện event storming cho luồng đặt đồ ăn (Customer, Restaurant,
Rider, Payment). Với mỗi bước, liệt kê:
- Command (hành động, dạng động từ mệnh lệnh, ví dụ "PlaceOrder")
- Actor thực hiện command đó
- Domain Event phát sinh (dạng quá khứ, ví dụ "OrderPlaced")
- Policy: event này có tự động kích hoạt command kế tiếp không, và command đó là gì

Trình bày dạng bảng, theo đúng trình tự thời gian.

Kết quả điển hình sẽ tạo ra chuỗi sự kiện: OrderPlacedPaymentAuthorizedOrderAcceptedRiderAssignedOrderPickedUpOrderDeliveredPaymentCaptured. Đây chính là chuỗi bạn nên hình ảnh hóa để chia sẻ với cả team không rành kỹ thuật:

flowchart LR
    A([Customer]) -->|PlaceOrder| E1[/OrderPlaced/]
    E1 -->|policy: auto-authorize| C1{{AuthorizePayment}}
    C1 --> E2[/PaymentAuthorized/]
    E2 -->|policy: notify restaurant| C2{{AcceptOrder}}
    C2 --> E3[/OrderAccepted/]
    E3 -->|policy: dispatch nearest rider| C3{{AssignRider}}
    C3 --> E4[/RiderAssigned/]
    E4 -->|Rider action| C4{{PickUpOrder}}
    C4 --> E5[/OrderPickedUp/]
    E5 -->|Rider action| C5{{DeliverOrder}}
    C5 --> E6[/OrderDelivered/]
    E6 -->|policy: capture funds| C6{{CapturePayment}}
    C6 --> E7[/PaymentCaptured/]

Nhìn chuỗi này, bạn sẽ lập tức thấy ranh giới bounded context (ranh giới ngữ cảnh nghiệp vụ, khái niệm cốt lõi của DDD) tự lộ ra: nhóm sự kiện OrderPlaced/OrderAccepted/OrderDelivered thuộc context "Order Management", trong khi PaymentAuthorized/PaymentCaptured thuộc context "Payments" — hai context này rất có thể nên là hai service khác nhau nếu bạn đi theo hướng microservices. Đây là giá trị thực sự của event storming: nó không chỉ vẽ luồng, nó gợi ý ranh giới hệ thống.

Lưu ý quan trọng: đừng để AI tự bịa ra policy. Sau khi có bảng nháp, luôn hỏi ngược "Policy nào trong danh sách này là giả định của bạn, cần tôi xác nhận với business?" — AI có xu hướng giả định luồng "happy path" mượt mà, trong khi thực tế luôn có nhánh lỗi (nhà hàng từ chối, thanh toán thất bại, tài xế hủy giữa chừng) mà bạn phải bổ sung thủ công.

Mẹo: Sau khi có chuỗi event chính (happy path), yêu cầu AI thêm một vòng "liệt kê các exception event có thể xảy ra tại mỗi bước" (ví dụ RestaurantRejectedOrder, PaymentDeclined, RiderCancelled) — đây là nơi 80% bug logic thực tế trốn.

Các Khái Niệm Domain-Driven Design (DDD) Với Sự Hỗ Trợ Của AI

DDD cho bạn một bộ từ vựng chung để nói về domain, và AI dùng bộ từ vựng này cực tốt — miễn là bạn hỏi đúng câu.

Bounded context là ranh giới trong đó một thuật ngữ có một nghĩa duy nhất và nhất quán. Trong hệ thống giao đồ ăn, từ "Order" trong context "Order Management" nghĩa là toàn bộ vòng đời đơn hàng; nhưng trong context "Analytics/Reporting", "Order" có thể chỉ là một bản ghi read-only phẳng dùng để tính doanh thu. Prompt hữu ích: "Dựa trên các entity và event đã liệt kê, đề xuất cách chia bounded context, chỉ ra thuật ngữ nào có nghĩa khác nhau giữa các context." AI thường gợi ý ba context: Ordering, Fulfillment (Rider/Delivery), Payments — mỗi context có ubiquitous language (ngôn ngữ chung, thuật ngữ thống nhất giữa domain expert và kỹ sư trong phạm vi một bounded context) riêng.

Aggregate và aggregate root (gốc tổng hợp — entity chịu trách nhiệm bảo vệ tính nhất quán của cả cụm entity liên quan) là khái niệm mà AI hay nhầm nếu bạn không hỏi rõ. Với domain của chúng ta, Order là aggregate root hợp lý của cụm Order + OrderItem, vì mọi thay đổi trên OrderItem phải đi qua Order để đảm bảo bất biến (invariant) như "tổng tiền luôn khớp với tổng các item". Prompt kiểu: "Trong domain model này, entity nào nên là aggregate root? Liệt kê invariant mà aggregate root đó phải bảo vệ" sẽ buộc AI suy nghĩ về tính nhất quán chứ không chỉ về cấu trúc bảng.

Value object vs entity là ranh giới hay bị bỏ qua nhất. Address giao hàng của một đơn cụ thể không cần định danh riêng, không đổi sau khi tạo — nó là value object, so sánh bằng giá trị. Ngược lại Customer cần định danh riêng (hai khách hàng trùng tên vẫn là hai người khác nhau) — nó là entity. Prompt: "Trong danh sách entity, cái nào có thể chuyển thành Value Object — dựa trên tiêu chí: có cần định danh duy nhất xuyên vòng đời không, có thể coi là bất biến (immutable) không." Việc downgrade đúng từ entity sang value object giúp giảm số bảng, giảm join, và code domain sạch hơn nhiều.

Prompt tổng hợp cho cả buổi làm việc DDD:

Dựa trên domain model giao đồ ăn (Customer, Restaurant, MenuItem, Order,
OrderItem, Rider, Payment) và chuỗi domain event đã có, hãy:
1. Đề xuất cách chia bounded context, và với mỗi context, liệt kê
   ubiquitous language riêng (từ nào có nghĩa khác nhau giữa các context).
2. Với mỗi context, chỉ ra aggregate root và invariant nó bảo vệ.
3. Chỉ ra entity nào nên là Value Object.
Giải thích lý do cho từng quyết định, đừng chỉ liệt kê kết luận.

Mẹo: Luôn bắt AI giải thích "vì sao" cho mỗi quyết định DDD (aggregate root, bounded context) chứ đừng chấp nhận danh sách trần — DDD là môn khoa học về trade-off, và nếu AI không giải thích được lý do, khả năng cao nó đang đoán theo pattern phổ biến chứ không theo domain thực của bạn.

Kiểm Chứng Data Model Với Business Rules

Có một schema đẹp không có nghĩa là schema đúng. Bước validate business rule là bước bị bỏ qua nhiều nhất, vì nó nhàm chán và đòi hỏi đọc lại requirements một lần nữa — chính vì thế đây là chỗ AI phát huy tốt nhất, vì nó không biết chán.

Cách làm: liệt kê toàn bộ business rule bằng lời (invariant, ràng buộc, luồng hợp lệ/không hợp lệ), sau đó yêu cầu AI đối chiếu từng rule với schema hiện tại, chỉ ra rule nào schema hiện tại KHÔNG enforce được (tức là rule chỉ tồn tại "trong đầu lập trình viên" chứ không có constraint tương ứng trong database).

Prompt ví dụ:

Đây là schema hiện tại (dán DDL). Đây là danh sách business rule:
1. Một Order không thể chuyển sang "delivered" nếu chưa có Payment ở
   trạng thái "captured".
2. total_amount của Order phải luôn bằng tổng (quantity * unit_price)
   của các OrderItem thuộc order đó.
3. Một MenuItem không available thì không được phép xuất hiện trong
   OrderItem mới.
4. Một Rider chỉ được gán cho tối đa 1 Order đang ở trạng thái active
   tại một thời điểm.

Với mỗi rule, cho biết: schema hiện tại có enforce được bằng
constraint/trigger/unique index không, hay chỉ có thể enforce ở
tầng application code? Nếu chỉ enforce được ở application, đề xuất
cách bổ sung ở tầng database nếu khả thi.

AI thường sẽ chỉ ra: rule 1 và rule 2 khó enforce thuần bằng constraint (cần trigger hoặc application-level check, vì chúng liên quan logic điều kiện phức tạp qua nhiều bảng); rule 3 có thể enforce một phần bằng trigger kiểm tra is_available tại thời điểm insert; rule 4 có thể enforce bằng partial unique index (CREATE UNIQUE INDEX ON orders (rider_id) WHERE status IN ('accepted','picked_up')) — một kỹ thuật Postgres rất mạnh mà nhiều kỹ sư quên mất là có tồn tại.

Đây chính là giá trị cốt lõi của bước validate: nó buộc bạn phân biệt rõ ràng "rule nào database bảo vệ được, rule nào chỉ code bảo vệ được" — và với rule loại hai, bạn biết mình cần viết test tự động thay vì tin vào constraint.

Mẹo: Với mỗi business rule không thể enforce bằng database constraint, yêu cầu AI viết luôn một test case (unit hoặc integration) kiểm tra rule đó — biến "rule bằng lời" thành "rule có thể chạy được" ngay trong cùng một phiên làm việc.

Hands-On: Một Buổi Domain Modeling Từ Requirements

Đến lúc ráp toàn bộ quy trình lại thành một buổi làm việc thực tế, từ một đoạn requirements ngắn tới schema hoàn chỉnh. Giả sử bạn nhận được đoạn mô tả sau từ product manager:

"Chúng ta cần tính năng cho phép khách đặt món từ nhà hàng gần nhất, thanh toán online, và theo dõi tài xế giao hàng theo thời gian thực. Nhà hàng có thể bật/tắt món trong menu tùy tình trạng nguyên liệu."

Các bước thực hiện:

  1. Trích xuất domain entities. Dùng prompt phần 2 để lấy danh sách entity, thuộc tính, quan hệ. Xác nhận với bản thân: Restaurant, MenuItem, Customer, Order, OrderItem, Rider, Payment — không có gì bất ngờ so với domain chúng ta đã dùng xuyên suốt bài viết.

  2. Chạy prompt phản biện để tìm entity còn thiếu. Prompt: "Dựa trên danh sách entity hiện tại, đoạn requirements có nhắc tới khái niệm 'theo dõi tài xế theo thời gian thực' — điều này có cần một entity mới không (ví dụ Location/Tracking event)?" AI thường trả lời cần thêm một entity RiderLocation hoặc DeliveryTrackingEvent — một bảng append-only lưu tọa độ theo thời gian, tách biệt khỏi bảng riders chính (vì tần suất ghi cực cao, không nên trộn vào bảng entity chính có ít thay đổi).

  3. Sinh ERD Mermaid bằng prompt phần 3, xác nhận cardinality bằng mắt: Order 1-n OrderItem, Order 0..1 Payment, Rider 1-n DeliveryTrackingEvent.

  4. Event storming nhanh bằng prompt phần 5, thêm bước RiderLocationUpdated vào giữa RiderAssignedOrderDelivered.

  5. Sinh DDL bằng prompt phần 4, nhớ nhắc rule tiền/thời gian/ID, và thêm yêu cầu riêng cho bảng tracking: "Bảng DeliveryTrackingEvent nên dùng index gì để truy vấn nhanh vị trí mới nhất của một rider?" — AI nên đề xuất index trên (rider_id, recorded_at DESC), hoặc gợi ý dùng một bảng riêng rider_current_location (chỉ 1 dòng/rider, update liên tục) tách khỏi bảng lịch sử để tránh quét bảng lớn mỗi lần cần vị trí hiện tại — đây là một denormalization hợp lý cho use case đọc tần suất cao.

  6. Validate business rule. Dùng prompt phần 7 với rule bổ sung: "DeliveryTrackingEvent chỉ được ghi khi Order đang ở trạng thái picked_up". AI sẽ chỉ ra rule này cần enforce ở application layer (kiểm tra status trước khi insert) vì constraint database không dễ tham chiếu chéo trạng thái từ bảng khác trong một CHECK đơn giản.

  7. Tổng hợp DDD. Chạy prompt phần 6, xác nhận RiderLocationUpdated/DeliveryTrackingEvent thuộc bounded context "Fulfillment/Tracking" — một context tách biệt, có thể scale độc lập (ghi rất nhiều, đọc theo pattern khác hẳn "Ordering").

Sau bảy bước này, từ một đoạn requirements ba câu, bạn có: danh sách entity đầy đủ (kể cả entity ẩn như tracking event mà bản thân requirements không nói thẳng), một ERD review được, một chuỗi event storming, DDL production-ready có index và constraint đúng chỗ, và một bộ business rule đã được đối chiếu rule-by-rule với schema. Đó là sự khác biệt giữa "dùng AI để viết code nhanh hơn" và "dùng AI để tư duy domain sâu hơn".

Mẹo: Lưu lại toàn bộ bảy bước trên (prompt + output) thành một file markdown trong repo (ví dụ docs/domain-model/food-delivery.md) — đây trở thành tài liệu sống, và lần sau thêm tính năng, bạn chỉ cần nạp lại file này cho AI thay vì giải thích domain từ đầu.

Những điểm chính

  • Domain modeling luôn phải đi trước data modeling — tách prompt thành hai bước riêng biệt, đừng để AI nhảy thẳng vào CREATE TABLE.
  • Trích xuất entity, thuộc tính, relationship từ requirements bằng prompt có cấu trúc; luôn hỏi ngược để phân biệt Entity thật với Value Object.
  • Mermaid erDiagram là công cụ nhanh nhất để hình ảnh hóa và review domain model cùng team, chạy được ngay trong GitHub/GitLab.
  • Khi thiết kế schema, luôn nhắc rõ ràng buộc: tiền dùng DECIMAL, thời gian dùng TIMESTAMPTZ, khóa chính UUID hay auto-increment tùy ngữ cảnh — AI sẽ không tự nhớ quy ước của bạn giữa các phiên.
  • Event storming với AI hiệu quả nhất khi dùng để nháp trước hoặc số hóa sau buổi họp trực tiếp, không thay thế hoàn toàn buổi họp có domain expert.
  • Các khái niệm DDD (bounded context, aggregate root, ubiquitous language, value object) chỉ có giá trị khi AI giải thích được lý do — đừng chấp nhận kết luận trần trụi.
  • Luôn đối chiếu business rule với schema để biết rule nào database enforce được, rule nào chỉ code enforce được, rồi biến rule loại hai thành test tự động.
  • Một buổi domain modeling hoàn chỉnh nên đi đủ bảy bước: entity → phản biện thiếu sót → ERD → event storming → schema → validate rule → tổng hợp DDD, và nên được lưu lại thành tài liệu sống trong repo.