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.
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ì?
| Dimension | Câu hỏi thiết kế | Ví dụ |
|---|---|---|
| Resource / action | Journey quan trọng nào cần public contract? | POST /orders, GET /orders/{id}, OrderPlaced |
| Request / response | Input 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 context | Ai đượ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 contract | Client phân biệt validation, conflict, missing, forbidden, transient failure ra sao? | Stable error code + human-readable message. |
| Idempotency | Retry request có được tạo side effect lần hai không? | Idempotency key cho create/payment-like mutation. |
| Pagination | Result 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 status | Operation lâu trả trạng thái/handle gì để client poll hoặc subscribe? | 202 Accepted + operation ID + status resource. |
| Concurrency | Làm sao tránh lost update? | Version field hoặc ETag + conditional update. |
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
| Journey | Lookup / range / sort | Write profile | Consistency | Data-model implication |
|---|---|---|---|---|
| Mở order detail | Lookup bằng order_id | Read-heavy | Read-your-writes thường quan trọng | Stable primary key; tránh scan theo non-key field. |
| Liệt kê order của user | user_id + time range, sort newest-first | Append thường xuyên | Có thể chấp nhận projection hơi trễ tùy UX | Composite index/partition path theo user_id và sortable time/ID. |
| Admin filter theo status | Filter status + tenant + time | Lower frequency nhưng broad result | Tùy operational need | Secondary index/search projection; không nhất thiết ép primary store tối ưu mọi filter. |
| Cập nhật state | Lookup aggregate ID | Write-sensitive | Strong invariant trong aggregate | Đặt dữ liệu cần atomic update trong cùng transaction/ownership boundary nếu 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
- Uniqueness: một email unique toàn hệ thống, theo tenant, hay chỉ unique trong một account?
- State machine: order được phép đi từ
PENDING → PAID → FULFILLED; transition nào bị cấm hoặc cần compensating action? - Transaction scope: những row/document/aggregate nào phải cùng thành công hoặc cùng thất bại?
- Source of truth: record nào quyết định state cuối cùng khi cache, search index, projection hoặc analytics disagree?
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ỏi | Design 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? |
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ạnh | Trade-off / caveat | Hỏi thêm trước khi chọn |
|---|---|---|---|
| Auto-increment | Compact, đơ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 UUID | Generate độ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 ID | Rough 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 code | Dễ đọ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? |
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
| Phase | Mục tiêu | Failure cần nghĩ tới |
|---|---|---|
| Expand | Thê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. |
| Backfill | Chuyể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. |
| Contract | Xó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
- Ưu tiên additive change khi có nhiều consumer deploy độc lập.
- Không tái sử dụng cùng field với semantic khác chỉ để “đỡ tạo version mới”.
- Consumer nên tolerate field chưa biết nếu protocol cho phép.
- Cache key, search projection và materialized view cần gắn version/epoch khi interpretation thay đổi.
- Backfill không được coi là xong chỉ vì job “SUCCESS”; cần đối chiếu count/checksum/business invariant phù hợp.
6. Design review checklist
- Critical API/events đã nói rõ request/response, auth context, error và retry/idempotency chưa?
- Pagination và async operation status có contract cụ thể chưa?
- Mỗi dominant journey đã map thành lookup/range/sort/filter, write frequency và consistency requirement chưa?
- Primary/partition/index choice có giải thích bằng access pattern thay vì “best practice” chung chung chưa?
- Source of truth, uniqueness, state machine và transaction scope có explicit chưa?
- Mỗi data domain có owner rõ; có shared-table coupling bị che bởi service diagram không?
- ID strategy có trade-off về distribution, predictability, locality, privacy và collision/abuse chưa?
- Schema/API/event migration có mixed-version plan, backfill throttle/checkpoint/reconciliation và rollback chưa?