Mở đầu — vì sao bài này quan trọng
Bạn đã đi qua 59 bài học. Bạn biết REST, JSON, HTTP method, status code, authentication, pagination, webhook, rate limiting, error design, idempotency, microservices, message queue, API Gateway, OWASP API Top 10, SLA và cả cách đọc một file OpenAPI. Mỗi mảnh ghép đó, đứng riêng, là một viên gạch. Nhưng một viên gạch không phải là một ngôi nhà. Bài Capstone này là lúc bạn xếp tất cả các viên gạch lại thành một thiết kế API hoàn chỉnh, end-to-end, cho một sản phẩm thật.
Đây chính xác là thứ phân biệt một BA "biết thuật ngữ" với một BA "thiết kế được hệ thống". Trong phỏng vấn cấp Senior BA hoặc Solution Analyst, người ta hiếm khi hỏi bạn "HTTP 201 nghĩa là gì". Người ta đưa cho bạn một bài toán nghiệp vụ mơ hồ và bảo: "Hãy phác thảo các API cần thiết". Lúc đó, năng lực của bạn không nằm ở việc nhớ định nghĩa, mà ở khả năng chia bài toán thành các resource, xác định luồng, chọn pattern phù hợp và lường trước rủi ro.
Trong bài này, chúng ta sẽ cùng làm một dự án xuyên suốt: thiết kế hệ thống API cho GoMart — một startup Việt Nam làm app giao siêu thị mini, cam kết giao hàng trong 30 phút. Tôi sẽ dẫn bạn đi từ tờ giấy trắng đến một bộ API spec mà bạn có thể mang vào phòng họp. Hãy coi đây là bài tập mô phỏng năng lực thực chiến của chính bạn.
Khái niệm cốt lõi
Tư duy "domain trước, API sau"
Sai lầm phổ biến nhất của BA mới là nhảy ngay vào việc liệt kê endpoint: GET /products, POST /orders... Đó là làm ngược. Một thiết kế end-to-end tốt luôn bắt đầu từ domain — tức là các thực thể (entity) và quy tắc nghiệp vụ — rồi mới sinh ra API.
Với GoMart, các thực thể cốt lõi (gọi là resource trong ngôn ngữ REST) gồm:
- Customer — khách hàng đặt hàng qua app.
- Store — cửa hàng/kho mini, mỗi quận có vài kho để đảm bảo bán kính giao 30 phút.
- Product — sản phẩm, có tồn kho theo từng Store (đây là chi tiết quan trọng: một SKU có thể còn hàng ở kho Quận 1 nhưng hết ở kho Quận 7).
- Cart — giỏ hàng tạm.
- Order — đơn hàng, trái tim của hệ thống.
- Driver — tài xế giao hàng.
- Payment — giao dịch thanh toán.
- Delivery — tiến trình giao, gắn với Order và Driver.
Bốn câu hỏi định hình mọi thiết kế
Trước khi viết bất kỳ endpoint nào, một BA giỏi tự trả lời bốn câu hỏi:
- Ai là consumer của API này? App mobile của khách, app của tài xế, dashboard nội bộ, và đối tác bên thứ ba (ví dụ cổng thanh toán) — bốn nhóm này có nhu cầu rất khác nhau. Đây là lý do người ta hay dùng pattern BFF (Backend for Frontend) — mỗi loại client có một lớp API riêng được tối ưu.
- Cái gì là đồng bộ, cái gì là bất đồng bộ? Đặt hàng và xem giỏ là đồng bộ (khách chờ kết quả ngay). Còn xử lý thanh toán, tìm tài xế, cập nhật vị trí giao hàng là bất đồng bộ — không thể bắt khách đứng chờ HTTP response trong khi hệ thống đi gọi tài xế.
- Sự kiện nào cần đẩy ngược về client? Trạng thái đơn thay đổi (đã xác nhận → đang lấy hàng → đang giao → đã giao) cần đẩy về app khách theo thời gian thực. Đây là chỗ dùng WebSocket hoặc webhook, không phải bắt app polling liên tục.
- Điều gì xảy ra khi có lỗi? Khách bấm "Đặt hàng" hai lần do mạng chậm thì sao? Thanh toán trừ tiền nhưng tạo đơn thất bại thì sao? Đây là chỗ idempotency và thiết kế error response lên tiếng.
Vòng đời một đơn hàng — xương sống của thiết kế
Với app giao hàng, mọi thứ xoay quanh state machine của Order. Hãy hình dung các trạng thái:
CREATED → PAID → CONFIRMED (kho nhận đơn) → PICKING (đang soạn hàng) → DRIVER_ASSIGNED → DELIVERING → DELIVERED, với các nhánh rẽ CANCELLED và REFUNDED.
Mỗi mũi tên chuyển trạng thái là một sự kiện nghiệp vụ, và mỗi sự kiện thường tương ứng với một API call hoặc một message trên hàng đợi. Khi bạn nắm state machine, bạn biết chính xác cần những API gì và ai được phép kích hoạt chuyển trạng thái nào (tài xế không được tự chuyển đơn sang REFUNDED).
Tình huống thực tế
Tình huống 1: Bài toán "đặt hàng trùng" của một app giao đồ ăn
Một công ty giao đồ ăn ở TP.HCM (giả định gần với một tên tuổi thật trên thị trường) gặp sự cố vào giờ cao điểm trưa: tỷ lệ đơn trùng tăng vọt lên khoảng 3%. Nguyên nhân: mạng 4G của khách chập chờn, khách bấm "Đặt hàng", không thấy phản hồi sau 5 giây nên bấm lại. App gửi hai request POST /orders giống hệt nhau, và backend tạo ra hai đơn — hai lần trừ tiền.
Diễn giải: Đây đúng là bài toán idempotency (bài 25). Cách team xử lý: app tự sinh một Idempotency-Key (một UUID) ngay khi khách bấm nút lần đầu, và gửi key này trong header của mọi lần retry. Backend lưu key trong Redis với TTL 24 giờ; nếu thấy key đã tồn tại, nó trả về kết quả của đơn cũ thay vì tạo đơn mới.
Bài học: Với GoMart, ngay từ bản thiết kế đầu tiên, endpoint POST /orders phải nhận Idempotency-Key. Một BA thiết kế end-to-end không đợi sự cố xảy ra rồi mới vá — bạn lường trước nó trong spec. Đây là tư duy "thiết kế cho thất bại" mà phỏng vấn system design rất thích.
Tình huống 2: Vì sao Grab không bắt app polling vị trí tài xế
Khi bạn theo dõi tài xế di chuyển trên bản đồ trong app gọi xe, có hai cách kỹ thuật: app cứ vài giây gọi GET /drivers/{id}/location (polling), hoặc server chủ động đẩy vị trí về app (WebSocket). Các nền tảng lớn như Grab chọn cách thứ hai cho luồng real-time tracking.
Diễn giải: Hãy tính nhẩm như một BA. Giả sử có 50.000 chuyến đang chạy đồng thời giờ cao điểm, mỗi app polling mỗi 3 giây → khoảng 16.000 request/giây chỉ để hỏi vị trí, phần lớn trả về dữ liệu không đổi. Lãng phí khổng lồ về băng thông và tải server. WebSocket giữ một kết nối mở, server chỉ đẩy khi vị trí thực sự thay đổi.
Bài học: Với GoMart, luồng "theo dõi tài xế giao hàng" nên dùng WebSocket (bài 30), còn các luồng khác như xem danh mục sản phẩm thì REST đồng bộ là đủ. Bài học lớn ở đây: không có một kiểu API đúng cho mọi luồng. Năng lực của BA là chọn đúng công cụ cho đúng luồng, và biện minh được lựa chọn bằng con số.
Tình huống 3: Tích hợp cổng thanh toán VN và bài toán webhook
GoMart tích hợp một cổng thanh toán nội địa (như VNPay hoặc MoMo). Khi khách thanh toán, app redirect khách sang trang của cổng. Sau khi khách trả tiền xong, làm sao GoMart biết để xác nhận đơn?
Diễn giải: Câu trả lời sai là "app gọi GET /payment/status liên tục". Câu trả lời đúng theo chuẩn ngành: cổng thanh toán gọi webhook ngược về một endpoint của GoMart, ví dụ POST /webhooks/payment, báo "giao dịch X đã thành công". Đây là async pattern (bài 26, 51). Nhưng có một cái bẫy: webhook đến từ Internet công cộng, nên GoMart phải xác thực chữ ký (signature) trong payload để chắc chắn nó thực sự đến từ cổng, không phải kẻ gian giả mạo để "xác nhận" một đơn chưa trả tiền.
Bài học: Một thiết kế end-to-end không dừng ở "API nội bộ của tôi". Nó bao gồm cả các điểm tích hợp với bên thứ ba, và mỗi điểm tích hợp đều có rủi ro bảo mật cần ghi rõ trong spec. BA nào quên xác thực signature của webhook sẽ tạo ra lỗ hổng cho phép đặt hàng miễn phí.
Hướng dẫn từng bước
Đây là quy trình bạn có thể áp dụng cho bất kỳ bài Capstone hay bài toán phỏng vấn nào.
Bước 1 — Làm rõ phạm vi (scope) và đặt câu hỏi. Đừng nhận đề bài rồi lao vào vẽ. Hỏi lại: GoMart phục vụ bao nhiêu thành phố? Có cần hỗ trợ nhiều phương thức thanh toán không? Quy mô dự kiến (ngàn đơn/ngày hay triệu đơn/ngày)? Người phỏng vấn đánh giá rất cao BA biết làm rõ scope trước. Với bài này, ta giả định: bắt đầu ở TP.HCM, ~5.000 đơn/ngày, thanh toán qua ví điện tử và COD.
Bước 2 — Liệt kê resource và vẽ quan hệ. Customer, Store, Product, Cart, Order, Driver, Payment, Delivery. Vẽ nhanh: một Order thuộc một Customer (N:1), một Order có nhiều OrderItem (1:N), một Order gắn một Delivery (1:1), một Store có nhiều Product với tồn kho riêng (M:N qua bảng inventory).
Bước 3 — Map resource thành endpoint theo chuẩn REST. Dùng danh từ số nhiều, method đúng nghĩa:
GET /stores?lat=&lng=— tìm kho gần nhất theo toạ độ.GET /stores/{id}/products?category=&page=— danh mục sản phẩm theo kho, có filter và pagination (bài 22, 23).POST /carts/{id}/items— thêm sản phẩm vào giỏ.POST /orders(kèmIdempotency-Key) — tạo đơn.GET /orders/{id}— xem chi tiết, dùng để app theo dõi trạng thái.POST /webhooks/payment— nhận callback thanh toán.PATCH /orders/{id}/status— chỉ hệ thống nội bộ và tài xế (đúng quyền) được gọi.
OrderCreated lên message queue (bài 39, 40). Service tồn kho lắng nghe để trừ stock; service điều phối lắng nghe để tìm tài xế. Việc tìm tài xế không nằm trong HTTP response của POST /orders — nó chạy nền. App khách nhận cập nhật trạng thái qua WebSocket.Bước 5 — Thiết kế xác thực và phân quyền. App khách dùng OAuth 2.0 / JWT (bài 17, 18). App tài xế dùng token riêng với scope khác. Webhook từ cổng thanh toán xác thực bằng signature. Dashboard nội bộ qua SSO (bài 46).
Bước 6 — Thiết kế error response và versioning. Thống nhất một cấu trúc lỗi (bài 24): { "error": { "code": "OUT_OF_STOCK", "message": "...", "details": [...] } }. Đặt version ngay từ đầu: /v1/orders (bài 21). Thêm rate limiting cho endpoint nhạy cảm như tạo đơn (bài 20).
Bước 7 — Bổ sung quan sát và SLA. Định nghĩa SLA (bài 55): ví dụ p95 của POST /orders < 800ms. Thêm logging, distributed tracing để lần theo một đơn xuyên qua các service (bài 49, 52). Đây là phần nhiều BA quên, nhưng nó cho thấy bạn nghĩ đến vận hành thật.
Bước 8 — Viết spec dưới dạng OpenAPI và review. Đóng gói tất cả vào một file OpenAPI/Swagger (bài 41) để dev và QA cùng đọc một nguồn sự thật. Tự chạy qua API design checklist (bài 56) trước khi trình bày.
Lỗi thường gặp & mẹo
Lỗi 1 — Nhảy vào endpoint mà bỏ qua domain. Bạn sẽ có một mớ API rời rạc, trùng lặp, thiếu trước hụt sau. Luôn vẽ resource và quan hệ trước.
Lỗi 2 — Đặt tên endpoint kiểu RPC. POST /createOrderAndAssignDriver là sai tư duy REST. Hãy tách thành resource và để event-driven xử lý phần điều phối. Endpoint nên xoay quanh danh từ, không phải động từ ghép.
Lỗi 3 — Bắt mọi thứ chạy đồng bộ. Nhồi tìm tài xế, gửi SMS, cập nhật loyalty point vào trong một HTTP request tạo đơn sẽ làm response chậm và dễ timeout. Tách phần chậm ra async.
Lỗi 4 — Quên idempotency cho thao tác tạo và thanh toán. Đây là lỗi kinh điển đã thấy ở Tình huống 1. Tiền bạc và tạo đơn luôn cần idempotency.
Lỗi 5 — Thiết kế "happy path" mà bỏ qua thất bại. Hết hàng giữa chừng, tài xế huỷ, thanh toán treo, mạng khách rớt — spec của bạn phải có câu trả lời cho từng tình huống. Người phỏng vấn sẽ cố tình hỏi "nếu... thì sao".
Mẹo 1 — Luôn biện minh bằng con số. Đừng nói "dùng WebSocket vì nó tốt hơn". Hãy nói "vì 50.000 phiên polling 3 giây/lần tạo 16.000 req/s lãng phí". Con số làm bạn thuyết phục.
Mẹo 2 — Bắt đầu đơn giản, rồi mở rộng. Vẽ phiên bản tối thiểu chạy được trước, sau đó người phỏng vấn đẩy quy mô lên thì bạn thêm cache (bài 34, 35), CDN (bài 36), load balancer (bài 37). Đừng over-engineer ngay từ đầu.
Mẹo 3 — Gắn mỗi quyết định với một bài đã học. Khi trình bày, nói rõ "ở đây tôi dùng cursor pagination thay vì offset vì dữ liệu thay đổi liên tục". Điều này cho thấy bạn hiểu vì sao, không chỉ cái gì.
Bài tập thực hành
- Vẽ state machine của Order cho GoMart. Liệt kê đầy đủ các trạng thái và mỗi mũi tên chuyển trạng thái tương ứng với API nào hoặc event nào. Đánh dấu ai (khách/tài xế/hệ thống) được phép kích hoạt mỗi chuyển trạng thái.
- Thiết kế endpoint
POST /ordersđầy đủ. Viết ra: request body mẫu (JSON), các header bắt buộc (gồmIdempotency-Key,Authorization), status code thành công, và ít nhất ba response lỗi (hết hàng, kho ngoài bán kính giao, thanh toán thất bại) theo cấu trúc error thống nhất.
- Chọn cơ chế giao tiếp cho ba luồng sau và biện minh: (a) khách xem danh mục sản phẩm, (b) khách theo dõi tài xế trên bản đồ, (c) nhận xác nhận thanh toán từ cổng VN. Với mỗi luồng, ghi rõ chọn REST đồng bộ / WebSocket / webhook và lý do bằng một câu có con số hoặc đặc điểm nghiệp vụ.
- Mở rộng quy mô. Giả sử GoMart tăng từ 5.000 lên 500.000 đơn/ngày và mở thêm 4 thành phố. Liệt kê ba thay đổi kiến trúc bạn sẽ đề xuất (gợi ý: cache, CDN, load balancer, tách microservice) và mỗi thay đổi giải quyết nút thắt nào.
- (Nâng cao) Viết bản nháp OpenAPI cho hai endpoint
GET /stores/{id}/productsvàPOST /orders, gồm parameters, request body schema và responses. Tự chấm điểm bằng API design checklist của bài 56.
Tóm tắt
Capstone không phải là học thêm kiến thức mới — nó là bài kiểm tra xem bạn đã kết nối được các mảnh kiến thức thành một thiết kế sống động hay chưa. Qua dự án GoMart, chúng ta đã thực hành quy trình tám bước: làm rõ scope → liệt kê resource và quan hệ → map thành endpoint REST → tách luồng async và event → thiết kế auth và phân quyền → chuẩn hoá error và versioning → bổ sung observability và SLA → đóng gói thành OpenAPI để review.
Ba bài học cốt lõi cần khắc ghi: (1) luôn bắt đầu từ domain, không phải từ endpoint; (2) không có một kiểu API đúng cho mọi luồng — REST, WebSocket, webhook và message queue mỗi cái có chỗ của nó, và bạn chọn bằng con số chứ không bằng cảm tính; (3) thiết kế cho thất bại, không chỉ cho happy path — idempotency, error response rõ ràng và xác thực điểm tích hợp là dấu hiệu của một BA trưởng thành.
Nếu bạn làm được bài tập số 2 và số 3 một cách trôi chảy, bạn đã sẵn sàng bước vào một buổi phỏng vấn system design cho vị trí BA, hoặc ngồi vào bàn thiết kế cùng team kỹ thuật với sự tự tin. Đó chính là đích đến của cả khoá học này: không phải để bạn trở thành lập trình viên, mà để bạn trở thành cây cầu vững chắc giữa nghiệp vụ và công nghệ. Chúc mừng bạn đã đi đến cuối hành trình.