Part 10 · System Design · 10.1.04

API contract và data model từ access patterns

API/data model là nơi requirements trở thành invariant, query và ownership cụ thể. Đừng bắt đầu bằng “tạo vài bảng rồi thêm CRUD”; hãy bắt đầu từ những hành vi quan trọng của hệ thống và những điều tuyệt đối không được phép sai.

Mental model: Requirement → critical API/event → access pattern → invariant/transaction boundary → key/index/partition → ownership → evolution plan. Mỗi lựa chọn ở data model phải giải thích được nó phục vụ journey nào và chấp nhận trade-off nào.

1. API surface

Ở system design, cần chỉ định các critical endpoints hoặc events và contract quan trọng của chúng: request/response, auth context, error, idempotency, pagination và trạng thái của async operation. Stable IDs cùng version/ETag hỗ trợ retry và concurrency. Không cần liệt kê toàn bộ CRUD nếu chúng không giúp chứng minh design.

Critical contract nên trả lời gì?

DimensionCâu hỏi thiết kếVí dụ
Resource / actionJourney quan trọng nào cần public contract?POST /orders, GET /orders/{id}, OrderPlaced
Request / responseInput tối thiểu là gì, output nào là stable?Không expose internal columns chỉ vì chúng đang có trong DB.
Auth contextAi được gọi? Tenant/user/service identity đi vào authorization thế nào?tenant_id lấy từ trusted auth context thay vì tin body từ client.
Error contractClient phân biệt validation, conflict, missing, forbidden, transient failure ra sao?Stable error code + human-readable message.
IdempotencyRetry request có được tạo side effect lần hai không?Idempotency key cho create/payment-like mutation.
PaginationResult lớn được đọc từng trang thế nào khi dữ liệu đang thay đổi?Cursor theo stable sort key thay vì offset cho high-churn feed.
Async statusOperation lâu trả trạng thái/handle gì để client poll hoặc subscribe?202 Accepted + operation ID + status resource.
ConcurrencyLàm sao tránh lost update?Version field hoặc ETag + conditional update.
Production note · research: HTTP conditional requests có thể dùng precondition cho cả thao tác thay đổi state nhằm tránh “lost update”; RFC 9110 mô tả cơ chế này. OpenAPI là interface description chuẩn, language-agnostic, phù hợp để làm rõ contract nhưng không thay thế business invariant hay runtime authorization.

Ví dụ contract tối thiểu

POST /orders
Authorization: Bearer ...
Idempotency-Key: 9fd2...

{
  "items": [{"sku":"A-17","quantity":2}],
  "shipping_address_id":"addr_42"
}

201 Created
ETag: "order-v1"
{
  "order_id":"ord_01...",
  "status":"PENDING",
  "created_at":"..."
}

Ví dụ trên chỉ minh họa cách nói rõ contract; endpoint, header và status cụ thể vẫn phải theo requirement của bài toán.

2. Access patterns

Với mỗi user/system journey, ghi rõ: key lookup, range query, sort/filter, write frequency và consistency. Từ dominant paths đó mới chọn primary key, partition key và index. Secondary index hoặc denormalization có thể làm read nhanh hơn nhưng đổi lại là write amplification, storage cost và consistency work.

Access-pattern matrix

JourneyLookup / range / sortWrite profileConsistencyData-model implication
Mở order detailLookup bằng order_idRead-heavyRead-your-writes thường quan trọngStable primary key; tránh scan theo non-key field.
Liệt kê order của useruser_id + time range, sort newest-firstAppend thường xuyênCó thể chấp nhận projection hơi trễ tùy UXComposite index/partition path theo user_id và sortable time/ID.
Admin filter theo statusFilter status + tenant + timeLower frequency nhưng broad resultTùy operational needSecondary index/search projection; không nhất thiết ép primary store tối ưu mọi filter.
Cập nhật stateLookup aggregate IDWrite-sensitiveStrong invariant trong aggregateĐặt dữ liệu cần atomic update trong cùng transaction/ownership boundary nếu có thể.
Anti-pattern: chọn partition key chỉ vì field đó “trông unique”. Partition key còn phải phân bố tải đủ đều và khớp dominant query. Một key phân bố dữ liệu tốt nhưng buộc phần lớn request fan-out qua mọi partition vẫn có thể là lựa chọn tệ.
Production note · research: hướng dẫn partitioning hiện đại nhấn mạnh việc phân tích access pattern trước, giảm cross-partition operation, chọn key có cardinality/distribution phù hợp và tránh hot partition. Đây là nguyên tắc bổ sung cho ý nguồn về dominant paths; chi tiết triển khai phụ thuộc data store cụ thể.

3. Invariant và ownership

Xác định rõ source of truth, uniqueness, state machine và transaction scope. Ví dụ, “ledger append + derived balance” là model khác fundamentally so với “mutable balance”: model đầu lưu lịch sử biến động làm nguồn sự thật rồi derive số dư; model sau cập nhật trực tiếp một giá trị hiện tại.

Biến requirement thành invariant

Ownership là một phần của data model

Data ownership quyết định service boundary. Nếu hai service cùng sửa một shared table, boundary trên sơ đồ chỉ là autonomy giả: deploy, migration, authorization và failure vẫn bị coupling qua cùng schema. Một service có thể publish event/read model cho service khác, nhưng cần biết ai là writer/source of truth.

Câu hỏiDesign tốt cần nói rõ
Ai được write?Một authoritative owner hay nhiều writer với conflict protocol?
Ai được read?Direct query, service API, replicated projection hay event stream?
Invariant nằm ở đâu?Database constraint, aggregate transaction, serialized command path, hoặc reconciliation?
Cross-service consistency?Synchronous transaction có thật sự khả thi hay cần saga/outbox/eventual consistency?
Interview rule: nếu nói “eventual consistency”, hãy chỉ rõ cái gì được phép eventual và invariant nào vẫn phải strong. “Toàn hệ thống eventual” thường là mô tả quá mơ hồ.

4. ID generation

Không có một chiến lược ID tốt nhất cho mọi workload. Nguồn nhấn mạnh bốn nhóm trade-off: auto-increment đơn giản nhưng centralized/predictable; UUID dễ sinh phân tán nhưng có cost về index locality/size; time-sortable IDs cải thiện locality nhưng mang timestamp/privacy/clock caveat; short code cần collision strategy và abuse controls.

LoạiĐiểm mạnhTrade-off / caveatHỏi thêm trước khi chọn
Auto-incrementCompact, đơn giản, index-friendly trong một DB.Centralized allocation; predictable; khó merge giữa nhiều writer nếu không có strategy.ID có lộ volume/order ra ngoài không? Multi-region write cần gì?
Random UUIDGenerate độc lập, namespace lớn, merge data dễ hơn.Key rộng hơn; random insertion có thể kém locality tùy index/storage engine.ID dùng internal hay public? Database/index implementation cụ thể?
Time-sortable IDRough creation-order locality; generate phân tán.Timestamp leakage, clock/monotonicity handling, library/version semantics.Có cần ordering thật hay chỉ locality? Timestamp lộ ra có acceptable?
Short codeDễ đọc/chia sẻ, hợp URL/coupon/code UX.Collision nếu random space nhỏ; enumeration/brute-force/abuse risk.Entropy bao nhiêu, retry collision thế nào, rate limit và expiry ra sao?
Production note · research: RFC 9562 chuẩn hóa UUIDv7 như một UUID time-ordered dùng Unix timestamp milliseconds ở phần high bits và phần còn lại cho uniqueness/monotonicity. Điều này làm “time-sortable UUID” bớt mơ hồ, nhưng không xóa các caveat của nguồn: timestamp exposure, clock behavior và đặc tính index vẫn cần đánh giá theo implementation.

ID không đồng nghĩa authorization

Dù ID là random UUID hay short opaque token, vẫn phải thực hiện authorization trên resource. “Khó đoán” chỉ làm enumeration khó hơn; nó không phải access control.

5. Evolution

Schema, API và event phải sống được trong giai đoạn mixed versions. Một rollout an toàn thường theo nhịp expand → deploy → backfill → switch → contract. Backfill cần throttle, checkpoint và reconciliation; cache/projection cũng cần versioning để tránh dữ liệu cũ và mới bị hiểu như cùng một schema.

Expand → deploy → backfill → switch → contract

PhaseMục tiêuFailure cần nghĩ tới
ExpandThêm field/table/index/endpoint/event shape mới theo cách backward-compatible.Old reader vẫn phải chạy; migration lock hoặc index build cost.
DeployĐưa code biết cả old/new shape; có thể dual-read/dual-write có kiểm soát.Partial deployment tạo mixed fleet; retry không được duplicate side effect.
BackfillChuyển historical data sang representation mới.Load spike, retry loop, missing row; cần throttle + checkpoint + reconciliation.
SwitchĐổi source read/write sang path mới sau khi verify.Rollback path và data drift cần đo được.
ContractXóa field/index/path cũ khi không còn consumer phụ thuộc.Consumer ẩn hoặc job cũ vẫn dùng contract cũ.

API/event compatibility

Migration trap: dual-write trực tiếp vào old và new store không tự động atomic. Nếu một write thành công và một write thất bại, bạn cần retry/reconciliation hoặc một pattern khác để xác định source of truth trong từng phase.

6. Design review checklist

Tài liệu: HTTP Semantics · RFC 9110 · OpenAPI Specification · Azure Data Architecture Guide · UUIDs · RFC 9562 · Azure · Data partitioning strategy