Rất nhiều đội ngũ viết contract với hai trạng thái cực đoan: hoặc quá mơ hồ nên mỗi người hiểu một kiểu, hoặc quá dày và quá sát code khiến tài liệu nhanh chóng chết ngay sau vài vòng triển khai. Vấn đề không nằm ở việc có nên viết contract hay không, mà nằm ở cách thiết kế contract để nó đủ rõ cho thi công, đủ bền để còn dùng được sau khi code thay đổi, và đủ hẹp để không ôm mọi thứ.
Với cách tiếp cận contract-first trong một software factory như Midi Coder, mục tiêu của contract không phải là mô tả toàn bộ thế giới. Mục tiêu là giảm bất ngờ ở đầu ra: ai làm cũng hiểu cùng một phạm vi, cùng ràng buộc, cùng cách kiểm tra đúng sai, cùng dấu vết traceability từ yêu cầu đến code, test và vận hành.
1. Contract tốt là gì?
Một contract dùng được cho thi công thường có bốn đặc điểm:
- Khóa đúng phạm vi: nói rõ nó quyết định phần nào và không quyết định phần nào.
- Dùng ngôn ngữ ổn định: ưu tiên khái niệm nghiệp vụ, rule, workflow, policy và schema hơn là mô tả cảm tính.
- Có cấu trúc để kiểm tra: mỗi phần có thể được map sang test, review hoặc một bước tự động hóa.
- Dễ bảo trì: sửa ở một chỗ, tác động được nhìn thấy ở các chỗ liên quan.
Nếu một contract không giúp đội thi công trả lời các câu hỏi như “đầu vào là gì”, “điều kiện nào bắt buộc”, “quyền nào được phép”, “luồng nào hợp lệ”, “API trả gì khi lỗi”, thì contract đó chưa đủ rõ. Nếu nó cố giải thích luôn mọi quyết định UI, kiến trúc, coding style, lịch sử thay đổi và chi tiết class nội bộ, nó đang đi quá xa phạm vi cần thiết.
2. Sáu lớp contract cốt lõi nên tách riêng
Một lỗi phổ biến là nhét hết mọi thứ vào một tài liệu “đặc tả tổng”. Cách làm hiệu quả hơn là tách thành các lớp contract có trách nhiệm rõ ràng.
2.1. Domain contract
Định nghĩa ngôn ngữ nghiệp vụ: thực thể, thuộc tính, trạng thái, quan hệ, bất biến. Đây là lớp trả lời câu hỏi “đối tượng nào tồn tại trong bài toán và điều gì luôn đúng”.
- Ví dụ tốt: định nghĩa một Request có các trạng thái draft, submitted, approved, rejected; không thể chuyển trực tiếp từ draft sang approved nếu chưa submitted.
- Ví dụ kém: “yêu cầu được xử lý theo luồng phù hợp” nhưng không nói trạng thái nào tồn tại.
2.2. App contract
Mô tả các use case của ứng dụng: ai kích hoạt, đầu vào tối thiểu, đầu ra mong đợi, precondition, postcondition. Đây là lớp giúp đội thi công hiểu hệ thống phải làm gì ở mức hành vi.
2.3. Rule contract
Chứa business rule có thể kiểm tra được. Rule contract nên càng ít văn xuôi càng tốt, vì đây là nơi đội triển khai và QA cần độ rõ tuyệt đối.
- Ví dụ: “Nếu loại hợp đồng là trial thì thời hạn tối đa là 30 ngày.”
- Không nên viết: “Thời hạn trial nên ngắn để giảm rủi ro.” Câu này là ý định, không phải rule có thể kiểm tra.
2.4. Workflow contract
Mô tả các bước, trạng thái, tác nhân và điều kiện chuyển trạng thái. Đây là phần rất quan trọng nếu hệ thống có duyệt, giao việc, escalation, retry hoặc tích hợp nhiều vai trò.
2.5. Policy contract
Áp cho RBAC contract và các quyết định quyền hạn. Nó trả lời ai được xem, tạo, sửa, duyệt, xóa, export hay chạy hành động đặc biệt nào, trong điều kiện nào.
2.6. API contract
Mô tả giao tiếp kỹ thuật: endpoint, method, schema request/response, lỗi chuẩn, idempotency, pagination, versioning. Đây là phần cần chặt chẽ vì chỉ một mơ hồ nhỏ cũng có thể làm chậm cả frontend, backend và tích hợp.
Khi sáu lớp này được tách bạch, mỗi nhóm biết mình đang đọc phần gì. Domain không bị lẫn với API. Rule không bị chôn trong ghi chú. Workflow không bị mô tả bằng vài câu cảm tính. Policy không bị gắn rải rác trong code review.
3. Khóa phạm vi và khóa ngôn ngữ để contract dùng được cho thi công
Contract chết thường chết từ đầu, ngay ở phần mở bài, vì người viết không khóa phạm vi. Muốn tránh điều đó, nên chốt bốn thứ càng sớm càng tốt:
- Phạm vi quyết định: tài liệu này quyết định cái gì.
- Phạm vi không quyết định: cái gì nằm ngoài tài liệu này.
- Từ điển thuật ngữ: mỗi thuật ngữ chỉ có một nghĩa trong ngữ cảnh này.
- Nguồn sự thật: nếu có xung đột, phần nào được ưu tiên.
Một công thức mở đầu rất hữu ích là:
Scope:
- Quyết định: trạng thái, rule duyệt, quyền thao tác, schema API công khai.
- Không quyết định: kiến trúc module nội bộ, UI chi tiết, tên class, tên bảng.
Glossary:
- Request: hồ sơ nghiệp vụ do người dùng tạo.
- Approver: vai trò có quyền phê duyệt.
- Submit: hành động chuyển từ draft sang submitted.Cách viết này giúp đội thi công biết họ nên bám vào đâu và không nên tranh luận lan sang đâu. Đây cũng là nền tảng để làm traceability: từ glossary sang rule, từ rule sang workflow, từ workflow sang API, từ API sang test.
4. Phần nào nên viết ngắn, phần nào bắt buộc phải rõ và có cấu trúc
Không phải mọi phần trong contract đều cần mức chi tiết như nhau.
Nên viết ngắn
- Bối cảnh: 1 đến 2 đoạn là đủ.
- Mục tiêu: nêu rõ kết quả mong muốn, không kể lịch sử dài dòng.
- Out of scope: liệt kê ngắn gọn, dứt khoát.
- Nguyên tắc thiết kế: tối đa vài ý, tránh biến thành manifesto.
Bắt buộc phải rõ và có cấu trúc
- Schema: field nào bắt buộc, kiểu dữ liệu, ràng buộc, enum.
- Rule: điều kiện, ngoại lệ, độ ưu tiên giữa các rule.
- Workflow: trạng thái, sự kiện, actor, guard condition.
- Policy/RBAC: role, action, resource, condition.
- API contract: request, response, status code, error code.
Nói ngắn gọn: phần nào cần kiểm thử, cần tích hợp, cần audit, cần phân quyền thì phải viết theo cấu trúc. Phần nào chỉ để định hướng thì nên gọn.
5. Một mẫu DSL contract tối giản nhưng đủ thi công
Nếu đội của bạn có nhiều module lặp lại, nên chuẩn hóa bằng DSL contract thay vì viết tự do mỗi nơi một kiểu. DSL không cần phức tạp; điều quan trọng là nhất quán.
domain Request {
fields:
id: uuid
type: enum[trial, official]
status: enum[draft, submitted, approved, rejected]
owner_id: uuid
duration_days: integer
}
rules {
R1: if type = trial then duration_days <= 30
R2: submit allowed only when owner_id exists
R3: approve allowed only when status = submitted
}
workflow RequestApproval {
draft -submit-> submitted
submitted -approve-> approved
submitted -reject-> rejected
}
policy {
staff: create, read_own, update_own when status = draft
approver: read_all, approve, reject when status = submitted
}
api POST /requests {
request: RequestCreate
response 201: RequestView
response 422: ValidationError
}Một DSL contract như vậy giúp nhiều việc diễn ra đồng thời: người phân tích đọc được, kỹ sư backend map sang validation và workflow engine được, frontend hiểu trạng thái để dựng màn hình, QA sinh test case được, và đội vận hành có căn cứ để trace một lỗi về đúng rule hoặc đúng policy.
6. Lỗi thường gặp khiến contract thành tài liệu chết
6.1. Cố ôm hết mọi thứ
Khi một tài liệu vừa muốn là BRD, vừa muốn là thiết kế kỹ thuật, vừa muốn là hướng dẫn UI, vừa muốn là checklist kiểm thử, nó sẽ nhanh chóng mất trọng tâm. Kết quả là không ai biết đâu là phần có tính quyết định.
6.2. Bám sát code quá mức
Nếu contract chứa tên class, tên hàm, tên biến nội bộ, nó sẽ hỏng theo từng lần refactor. Contract nên ổn định hơn code. Nó mô tả cam kết và hành vi, không phải ảnh chụp chi tiết cài đặt.
6.3. Viết rule bằng ngôn ngữ mơ hồ
Các cụm như “hợp lý”, “phù hợp”, “nếu cần”, “thông thường” gần như không có giá trị thi công. Rule phải được viết sao cho hai người khác nhau đọc vào vẫn kiểm tra ra cùng một kết luận.
6.4. Không có thứ tự ưu tiên khi rule xung đột
Ví dụ policy cho phép sửa bản ghi của mình, nhưng workflow lại cấm sửa sau khi đã submit. Nếu không nói rõ ưu tiên, đội thi công sẽ phải đoán. Và hễ có đoán là có bất nhất.
6.5. Không gắn traceability
Mỗi rule quan trọng nên có mã định danh như R1, R2, P1, WF1. Nhờ đó commit, test case, bug report, checklist review có thể trỏ về cùng một contract. Đây là điểm nhiều đội bỏ qua nhưng lại cực kỳ quan trọng trong mô hình software factory.
7. Contract tốt và contract tệ khác nhau ở đâu?
Ví dụ contract tệ
Người dùng tạo yêu cầu. Hệ thống kiểm tra hợp lệ và chuyển các bước phù hợp.
Người có quyền có thể duyệt. API trả lỗi nếu dữ liệu sai.Đoạn này nghe có vẻ đúng, nhưng gần như không thi công được. Không biết “hợp lệ” là gì, “các bước phù hợp” là bước nào, “người có quyền” là ai, “lỗi” là lỗi nào.
Ví dụ contract tốt hơn
R1: Request.type = trial => Request.duration_days <= 30
R2: submit only when owner_id is not null
WF1: draft -submit-> submitted
WF2: submitted -approve-> approved
P1: staff can update own request only when status = draft
P2: approver can approve only when status = submitted
API-POST-/requests:
201 => RequestView
422/INVALID_DURATION when R1 violated
403/FORBIDDEN when policy violatedBản tốt hơn không dài hơn quá nhiều, nhưng mọi điểm quyết định đều đã hiện ra: rule, workflow, policy và API contract liên kết với nhau. Đó là khác biệt giữa tài liệu tham khảo chung chung và tài liệu có thể dùng để thi công.
8. Cách giữ contract sống sau khi bắt đầu code
Muốn contract không chết, đừng xem nó là tài liệu bàn giao một lần. Hãy biến nó thành một phần của vòng đời phát triển:
- Review contract trước khi chia việc.
- Gắn mã rule vào test case và pull request.
- Đặt checklist rằng thay đổi workflow, policy hoặc API phải cập nhật contract tương ứng.
- Ưu tiên contract có cấu trúc để có thể sinh schema, mock, validator hoặc tài liệu tích hợp.
- Giữ một nguồn sự thật cho từng lớp contract thay vì copy đi nhiều nơi.
Trong thực tế, contract sống không nhất thiết phải hoàn hảo. Nó chỉ cần đủ ngắn để còn được cập nhật, đủ rõ để không ai phải đoán, và đủ cấu trúc để máy móc có thể hỗ trợ kiểm tra.
Kết luận
Contract tốt là contract làm cho code đầu ra ít bất ngờ hơn. Muốn vậy, hãy tách đúng lớp contract, khóa phạm vi ngay từ đầu, dùng ngôn ngữ kiểm tra được, viết có cấu trúc ở những phần ảnh hưởng trực tiếp đến thi công, và gắn traceability để mọi thay đổi đều lần ngược được về cam kết ban đầu.
Nếu phải nhớ một nguyên tắc duy nhất, hãy nhớ điều này: contract không cần dài để có giá trị; contract cần rõ, có ranh giới và có khả năng được sử dụng lặp lại trong quá trình làm sản phẩm.