Tình huống: cùng endpoint, hai câu chuyện khác nhau
Sprint review tháng 4. Backend dev vừa demo xong tính năng POST /api/v1/orders/bulk-import — endpoint nhận file CSV, validate, và tạo đơn hàng bulk. Mọi thứ pass test, ready to merge.
Frontend dev raise hand: "Anh, em thấy API trả về lỗi validation theo format khác với các endpoint khác trong hệ thống. Backend dùng errors[] array, nhưng các endpoint cũ dùng error singular. Component xử lý lỗi của em hardcode format cũ."
Backend dev: "Ủa format mới đúng theo RFC 7807 mà anh, anh đổi từ sprint trước rồi."
Frontend dev: "Sprint trước thì em không biết. Không thấy ghi ở đâu hết."
Đây là câu chuyện không hiếm trong bất kỳ team fullstack nào. Không phải ai lười biếng — là hai bên đang làm việc với cùng một hệ thống nhưng knowledge về system nằm ở hai nơi khác nhau, không kết nối. Backend dev biết tại sao đổi error format. Frontend dev biết component nào bị ảnh hưởng. Nhưng không ai biết của nhau.
Project chúng tôi đang nói đến là một hệ thống quản lý chuỗi bán lẻ — .NET 8 backend, Vue.js 3 frontend, PostgreSQL, Azure. Team gồm 3 backend dev, 2 frontend dev, và 1 BA. Đủ lớn để knowledge silo trở thành vấn đề thực sự, không đủ lớn để justify một documentation team riêng.
Bối cảnh: tại sao fullstack project khó hơn
Ở các bài trước, chúng tôi đã nói về vault cho base libraries và cross-project knowledge. Fullstack project thêm một chiều phức tạp: cùng một concept được hiểu theo hai layer kỹ thuật hoàn toàn khác nhau.
Ví dụ: concept "một đơn hàng có trạng thái pending_payment":
| Góc nhìn backend | Góc nhìn frontend |
|---|---|
Enum OrderStatus.PendingPayment trong domain model |
Badge component với màu vàng + text "Chờ thanh toán" |
| Business rule: timeout 30 phút, sau đó auto-cancel | Countdown timer component + websocket để nhận cancel event |
SQL: index trên (status, created_at) cho query performance |
API polling interval: 5 giây khi user đang trên trang order detail |
| Tại sao 30 phút: theo policy của payment gateway | UX decision: show warning khi còn 5 phút — ai quyết định? |
Nếu vault chỉ có notes của backend hoặc frontend, Claude chỉ hiểu được một nửa hệ thống. Khi một developer cần context đầy đủ — "tại sao feature này hoạt động như vậy" — họ phải hỏi hai người.
Giải pháp: vault với dual-perspective notes
Chúng tôi thiết kế vault theo nguyên tắc "một concept, nhiều góc nhìn, một chỗ."
project-retail-chain-vault/
│
├── CLAUDE.md # Hướng dẫn Claude — xem bên dưới
│
├── domain/ # Business concepts — trung lập BE/FE
│ ├── order/
│ │ ├── overview.md # Domain overview: entity, lifecycle, rules
│ │ ├── status-lifecycle.md # State machine: trạng thái và transitions
│ │ └── adr/
│ │ └── ADR-0007-pending-timeout-30min.md
│ ├── product/
│ ├── inventory/
│ └── customer/
│
├── backend/ # .NET 8 implementation notes
│ ├── api/
│ │ ├── orders/
│ │ │ ├── bulk-import-endpoint.md # POST /api/v1/orders/bulk-import
│ │ │ ├── error-format-rfc7807.md # ADR về error format change
│ │ │ └── status-query-endpoint.md
│ │ └── _conventions/
│ │ ├── error-response-format.md # Chuẩn error response toàn API
│ │ └── pagination-pattern.md
│ ├── domain/
│ │ ├── order-service.md
│ │ └── payment-timeout-job.md
│ └── database/
│ ├── order-indexes.md
│ └── migration-notes.md
│
├── frontend/ # Vue.js 3 implementation notes
│ ├── components/
│ │ ├── order/
│ │ │ ├── OrderStatusBadge.md # Liên kết với domain/order/status-lifecycle
│ │ │ ├── OrderCountdownTimer.md # Logic timer + websocket
│ │ │ └── BulkImportForm.md # Liên kết với backend/api/orders/bulk-import-endpoint
│ │ └── _conventions/
│ │ ├── error-handling-pattern.md # Cách xử lý API error trong components
│ │ └── api-composable-pattern.md
│ ├── composables/
│ │ ├── useOrderStatus.md
│ │ └── useBulkImport.md
│ └── state/
│ └── order-store.md
│
├── contracts/ # API contracts — nơi BE và FE gặp nhau
│ ├── orders/
│ │ ├── bulk-import-contract.md # Request/response schema, error codes
│ │ └── status-update-contract.md
│ └── _templates/
│ └── contract-template.md
│
└── _agent/
└── memory/
contracts/ folder là phần quan trọng nhất — đây là nơi backend và frontend gặp nhau trong vault. Mỗi contract note là "ngôn ngữ chung" giữa hai bên, và Claude dùng nó làm anchor khi traverse graph.
Chi tiết kỹ thuật: anatomy của một contract note
---
date: 2026-04-15
tags: [contract, api, orders, breaking-change]
backend_impl: [[backend/api/orders/bulk-import-endpoint]]
frontend_impl: [[frontend/components/order/BulkImportForm]]
domain: [[domain/order/overview]]
status: active
version: v1.2
---
# API contract: POST /api/v1/orders/bulk-import
## Mô tả
Upload CSV để tạo đơn hàng hàng loạt. Xử lý async qua background job.
## Request
POST /api/v1/orders/bulk-import
Content-Type: multipart/form-data
file:
warehouse_id:
## Response thành công (202 Accepted)
{
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "queued",
"estimated_rows": 150,
"check_status_url": "/api/v1/jobs/550e8400..."
}
## Error format (RFC 7807)
{
"type": "https://errors.retailchain.vn/validation",
"title": "Validation failed",
"status": 422,
"errors": [
{ "field": "row_5.product_code", "message": "Product not found" }
]
}
⚠️ **BREAKING CHANGE từ v1.1**: `error` singular → `errors` array.
Frontend cần update error handling. Xem: [[frontend/components/_conventions/error-handling-pattern]]
## Giới hạn
- File size: max 5MB
- Rows: max 500 rows per request
- Rate limit: 10 requests/minute per warehouse
## Lý do design (xem ADR đầy đủ tại backend)
Async vì import 500 rows có thể mất 30–60 giây — sync request sẽ timeout.
202 Accepted là đúng HTTP semantics cho async operation.
Note này là bi-directional: backend dev viết phần response format và lý do design, frontend dev thêm note về breaking change và link đến error handling pattern. Claude đọc một note này có đủ context của cả hai bên.
CLAUDE.md với dual-persona awareness
CLAUDE.md của project này có thêm phần hướng dẫn Claude về context khi làm việc với từng role:
# Retail Chain Project Vault — CLAUDE.md
## Tech stack
- Backend: .NET 8, C#, Dapper + EF Core, PostgreSQL, Azure Service Bus
- Frontend: Vue.js 3, TypeScript, Pinia, Vite
- API: RESTful, error format RFC 7807 (từ sprint 12)
## Cấu trúc vault và thứ tự đọc
1. `domain/` — đọc trước để hiểu business concept
2. `contracts/` — API contract giữa BE và FE
3. `backend/` hoặc `frontend/` — implementation detail tùy context
## Khi Claude làm việc với backend developer
- Luôn check `contracts/` trước khi suggest thay đổi API shape
- Nếu thay đổi ảnh hưởng response format → flag "BREAKING CHANGE cần update contracts/"
- Link implementation notes đến contract tương ứng
## Khi Claude làm việc với frontend developer
- Luôn đọc `contracts/` và `domain/` trước khi viết composable
- Nếu gặp behavior API không rõ → check `backend/api/` trước khi hỏi BE dev
- Error handling → xem `frontend/components/_conventions/error-handling-pattern`
## Quy tắc viết note (bắt buộc cho cả BE và FE)
- Contract note PHẢI có link 2 chiều: `backend_impl` và `frontend_impl`
- Breaking change PHẢI được tag `#breaking-change` + note trong `contracts/`
- Domain note PHẢI trung lập — không mention implementation detail của BE hay FE
## Active sprint (update 2026-06-20)
Sprint 18 — focus: payment integration, checkout flow
- BE: [[backend/api/payments/vnpay-webhook-endpoint]]
- FE: [[frontend/components/checkout/PaymentStatusPoller]]
- Contract: [[contracts/payments/vnpay-callback-contract]]
Phần "khi Claude làm việc với backend/frontend developer" là key insight từ thực tế: cùng một vault nhưng Claude nên traverse theo thứ tự khác nhau tùy context. Backend dev hỏi về API → Claude đọc từ domain xuống, check contract trước. Frontend dev hỏi về component → Claude đọc contract trước để validate expectation.
Pattern hay: "contract-first note" khi có disagreement
Một pattern chúng tôi tìm ra sau vài lần conflict: khi BE và FE disagree về behavior của một tính năng, thay vì Slack thread dài, chúng tôi tạo contract note draft và để Claude mediate.
Ví dụ thực tế sprint 16: tranh luận về behavior khi user thêm sản phẩm hết hàng vào giỏ hàng. BE muốn trả 422 ngay, FE muốn cho phép thêm vào giỏ nhưng warning khi checkout.
# [DRAFT] Contract: add-to-cart khi sản phẩm out-of-stock
## Proposal BE
Status 422 + error message ngay khi POST /cart/items
Lý do: ngăn inconsistent cart state, đơn giản hóa business logic
## Proposal FE
Allow thêm vào giỏ, validate khi checkout
Lý do: UX tốt hơn, user có thể mua sau khi restock
## Business context
[[domain/inventory/restock-notification]] — chúng ta có feature notify khi restock
User journey: nhiều user add-to-cart → chờ restock → checkout
Khi Claude đọc note này + domain context, nó point out: "Theo domain/inventory/restock-notification, có use case user intentionally để sản phẩm trong giỏ chờ restock — behavior của FE align với use case này hơn." Cuộc tranh luận giải quyết trong 15 phút, không phải 2 ngày Slack thread.
Bài học rút ra
Cái hoạt động tốt:
contracts/ folder làm giảm đáng kể friction giữa BE và FE. Trước đây, mỗi sprint planning tốn 30–45 phút để sync về API shape. Bây giờ frontend dev đọc contract note trước buổi họp, câu hỏi ít hơn và cụ thể hơn.
Domain notes trung lập tạo ra "ngôn ngữ chung" cho cả team. BA đọc được, BE đọc được, FE đọc được — không ai phải dịch lại cho ai. Claude làm trọng tài khi có ambiguity trong domain rules.
Cái phải làm lại:
Contract notes ban đầu bị "owned" bởi backend — FE chỉ đọc, không contribute. Phải explicitly assign "FE perspective" trong template để FE dev viết thêm phần "gotchas khi implement ở frontend". Sau khi thêm section đó vào template, contribution từ FE tăng rõ rệt.
Vault này lớn hơn vault base libraries — gần 600 notes sau 6 tháng. Tốc độ note creation nhanh hơn tốc độ maintenance. Chúng tôi đang thử nghiệm "Claude weekly audit": cuối sprint, Claude đọc toàn bộ notes được tạo trong sprint đó, check xem có note nào thiếu cross-link hoặc contract note nào chưa có frontend_impl pointer.
Takeaway
Knowledge graph fullstack không chỉ là "thêm nhiều notes hơn" — nó cần cấu trúc phản ánh được architecture thực sự của hệ thống. Với fullstack project, có 3 lớp cần kết nối: domain (business concept), contracts (API interface), và implementations (BE + FE). Khi Claude có thể traverse cả ba lớp, nó mới thực sự trả lời được câu hỏi "tại sao feature này hoạt động như vậy" — không chỉ từ một góc nhìn.
Nếu bạn đang build fullstack project và đang gặp friction giữa BE và FE knowledge, contracts/ folder là chỗ tốt để bắt đầu. Không cần vault hoàn chỉnh ngay — chỉ cần document API contract của 3–5 endpoints phức tạp nhất, link hai chiều với implementation, và thêm phần "breaking change history". Sau 2 sprint bạn sẽ thấy sự khác biệt.
Hai bài trước trong series này nếu bạn chưa đọc: 4 pattern knowledge graph với Claude + Obsidian (setup từ đầu) và knowledge graph xuyên base libraries (scale lên nhiều project).
Son Do — BKGlobal Tech Team
#BKGlobal #AI #architecture #obsidian #vuejs #dotnet #fullstack #knowledgemanagement #1percentbetter