Chương 17. Gọi Trực Tiếp: HTTP/API

Từ chương này, ta bước sang một phần mới:

> Các cách giao tiếp trong hệ thống.

Khi hệ thống chỉ có một backend và một database, mọi thứ thường khá đơn giản:

Frontend -> Backend -> Database

Nhưng khi hệ thống lớn hơn, sẽ có nhiều phần:

Frontend
-> API
-> Ordering
-> Payment
-> Inventory
-> Delivery
-> Notification
-> AI Judge

Lúc này, câu hỏi rất quan trọng là:

> Các phần này nói chuyện với nhau bằng cách nào?

Cách dễ hiểu nhất là gọi trực tiếp qua HTTP/API.

Ví dụ:

Ordering gọi Payment API
Payment trả kết quả ngay
Ordering tiếp tục xử lý

HTTP/API rất phổ biến vì nó đơn giản, rõ ràng, dễ debug, dễ test, dễ dùng từ nhiều ngôn ngữ.

Nhưng nó cũng có một cái bẫy:

> Càng dễ gọi trực tiếp, ta càng dễ biến hệ thống thành một chuỗi gọi nhau dài, chậm, và dễ đổ dây chuyền.

Chương này sẽ giúp ta hiểu:

  • HTTP/API phù hợp khi nào.
  • Không phù hợp khi nào.
  • Gọi trực tiếp khác queue/event ở đâu.
  • Vì sao cần timeout.
  • Retry nguy hiểm thế nào.
  • Idempotency liên quan gì.
  • API contract phải rõ ra sao.
  • Làm sao tránh microservices gọi nhau rối như mạng nhện.

---

17.1. HTTP/API là gì trong kiến trúc hệ thống?

HTTP/API là cách một bên gửi request đến bên khác và chờ response.

Ví dụ:

Client -> Server: Tôi muốn tạo đơn hàng
Server -> Client: Đơn hàng đã được tạo, mã là order_123

Hoặc giữa hai service:

Ordering Service -> Payment Service: Tạo payment cho order_123
Payment Service -> Ordering Service: Đây là payment_url

Mô hình này là request/response:

  • Có bên gọi.
  • Có bên được gọi.
  • Bên gọi thường chờ kết quả.
  • Nếu bên được gọi chậm, bên gọi cũng chậm.
  • Nếu bên được gọi lỗi, bên gọi phải quyết định làm gì.

HTTP/API rất hợp với câu hỏi kiểu:

> Tôi cần biết kết quả ngay để tiếp tục.

Ví dụ:

  • Đăng nhập.
  • Lấy thông tin sản phẩm.
  • Tạo đơn và trả order id.
  • Kiểm tra quyền truy cập.
  • Lấy payment URL.
  • Lấy danh sách bài học.
  • Gửi request chấm bài và nhận job id.

Nhưng không phải việc gì cũng nên làm bằng HTTP đồng bộ.

---

17.2. Ví dụ quán bánh: hỏi trực tiếp

Khách bấm đặt bánh.

Frontend gọi backend:

POST /orders

Backend trả:

201 Created
{
  "order_id": "order_123",
  "status": "confirmed"
}

Đây là HTTP/API rất tự nhiên.

Người dùng cần biết:

  • Đơn có được tạo không?
  • Mã đơn là gì?
  • Bước tiếp theo là gì?

Nhưng sau khi đơn được tạo, có nhiều việc khác:

  • Gửi email xác nhận.
  • Báo bếp.
  • Cập nhật analytics.
  • Đồng bộ CRM.
  • Tạo notification.

Những việc này không nhất thiết phải làm xong trước khi trả response cho khách.

Nếu backend vừa tạo đơn vừa gửi email vừa đồng bộ CRM vừa gọi analytics rồi mới trả response, khách sẽ phải đợi lâu hơn.

Tệ hơn, nếu CRM lỗi, đặt hàng có nên thất bại không?

Thường là không.

Vì vậy:

Tạo đơn: HTTP đồng bộ.
Gửi email/analytics/CRM: event hoặc queue phía sau.

Đây là cách nghĩ thực dụng.

---

17.3. Khi nào HTTP/API là lựa chọn tốt?

HTTP/API tốt khi:

  • Người gọi cần kết quả ngay.
  • Thao tác ngắn.
  • Bên được gọi sẵn sàng trả lời nhanh.
  • Quan hệ giữa hai bên rõ ràng.
  • Request/response dễ hiểu hơn event.
  • Cần truy vấn dữ liệu hiện tại.

Ví dụ:

Frontend lấy danh sách sản phẩm
Frontend gửi form đăng nhập
Admin xem chi tiết đơn
Ordering hỏi Catalog thông tin sản phẩm
Frontend lấy kết quả chấm bài theo job_id
API Gateway gọi Auth để kiểm tra token

HTTP/API đặc biệt hợp với truy vấn đọc:

GET /products
GET /orders/{id}
GET /grading-jobs/{id}
GET /users/me

Vì truy vấn đọc thường cần trả dữ liệu ngay cho màn hình.

HTTP/API cũng hợp với command ngắn:

POST /orders
POST /login
POST /submissions
POST /grading-jobs

Miễn là command đó không ôm việc lâu trong request.

---

17.4. Khi nào HTTP/API không phải lựa chọn tốt?

HTTP/API không tốt khi:

  • Việc xử lý lâu.
  • Không cần kết quả ngay.
  • Có nhiều bên cần phản ứng.
  • Bên được gọi dễ chậm hoặc không ổn định.
  • Cần retry nhiều lần.
  • Muốn chống đổ dây chuyền.
  • Muốn xử lý nền với concurrency riêng.

Ví dụ không nên giữ trong request chính:

  • Chấm bài AI mất 1 phút 30 giây.
  • Gửi hàng nghìn email.
  • Render video.
  • Đồng bộ dữ liệu sang hệ thống ngoài.
  • Tạo báo cáo nặng.
  • Crawl website.
  • Tính analytics lớn.
  • Gọi nhiều provider bên ngoài.

Với những việc này, cách tốt hơn thường là:

HTTP nhận yêu cầu
-> tạo job
-> trả job_id
-> worker xử lý sau
-> frontend poll hoặc nhận notification

Ví dụ AI Judge:

POST /submissions
-> tạo submission
-> tạo grading job
-> trả job_id ngay

Worker
-> gọi Gemini API
-> lưu kết quả

Frontend
-> GET /grading-jobs/{id}
-> xem trạng thái

HTTP vẫn có trong luồng, nhưng không dùng để giữ kết nối chờ 90 giây.

---

17.5. HTTP đồng bộ tạo ra coupling thời gian

Khi service A gọi HTTP đến service B, A phụ thuộc vào B tại thời điểm đó.

A -> B

Nếu B chậm, A chậm.

Nếu B down, A phải xử lý lỗi.

Nếu mạng giữa A và B lỗi, A cũng bị ảnh hưởng.

Đây gọi là temporal coupling: phụ thuộc theo thời gian.

Nói dễ hiểu:

> Muốn A chạy được ngay bây giờ, B cũng phải sống khỏe ngay bây giờ.

Với một vài call quan trọng, điều này bình thường.

Nhưng nếu một request phải gọi nhiều service:

Frontend
-> API
-> Ordering
-> Catalog
-> Inventory
-> Payment
-> Delivery
-> Notification

thì request đó chỉ nhanh và ổn định nếu tất cả cùng nhanh và ổn định.

Chỉ cần một service chậm, cả luồng chậm.

Đây là lý do HTTP direct call không nên bị lạm dụng.

---

17.6. Gọi dây chuyền nguy hiểm thế nào?

Giả sử một request checkout đi qua 5 service.

Mỗi service mất trung bình 200ms.

Nếu gọi tuần tự:

Ordering -> Catalog: 200ms
Ordering -> Inventory: 200ms
Ordering -> Payment: 200ms
Ordering -> Delivery: 200ms
Ordering -> Notification: 200ms

Tổng đã là khoảng 1 giây, chưa tính network, database, biến động.

Nếu một service thỉnh thoảng mất 2 giây, toàn bộ request bị kéo theo.

Nếu một service timeout, phải quyết định rollback hay tiếp tục.

Nếu request tăng cao, các service phía sau bị dồn tải.

Gọi dây chuyền còn làm debug khó:

User thấy checkout chậm.
Lỗi ở API Gateway?
Ordering?
Catalog?
Inventory?
Payment?
Network?
Database của service nào?

Vì vậy, khi thiết kế HTTP giữa service, hãy hỏi:

> Call này có thật sự cần nằm trên đường chờ của người dùng không?

Nếu không, cân nhắc queue/event.

---

17.7. Đừng để service gọi nhau như mạng nhện

Một hệ thống microservices xấu thường trông như:

Order gọi Payment
Payment gọi Order
Order gọi Inventory
Inventory gọi Catalog
Catalog gọi Pricing
Pricing gọi Promotion
Promotion gọi User
User gọi Order
Notification gọi User
Support gọi Order

Khi service nào cũng gọi service nào, hệ thống rất khó hiểu.

Dấu hiệu:

  • Không biết service nào là nguồn sự thật.
  • Sửa một API làm nhiều service vỡ.
  • Deploy một service phải kiểm tra rất nhiều bên.
  • Một request đi qua quá nhiều hop.
  • Lỗi lan dây chuyền.
  • Không có sơ đồ dependency rõ.

HTTP/API không sai.

Sai là dùng HTTP như dây nối tùy tiện giữa mọi thứ.

Một nguyên tắc tốt:

> Service nên gọi trực tiếp khi có quan hệ rõ và cần kết quả ngay. Những phản ứng phụ nên đi qua event/queue.

---

17.8. HTTP call là một contract

API không chỉ là đường dẫn.

API là hợp đồng giữa bên gọi và bên được gọi.

Ví dụ:

POST /payments
Request:
{
  "order_id": "order_123",
  "amount": 350000,
  "currency": "VND"
}

Response:
{
  "payment_id": "pay_456",
  "status": "created",
  "payment_url": "..."
}

Contract gồm:

  • Endpoint.
  • Method.
  • Request schema.
  • Response schema.
  • Error format.
  • Status code.
  • Authentication.
  • Timeout expectation.
  • Idempotency rule.
  • Versioning.

Nếu contract không rõ, hai bên sẽ hiểu khác nhau.

Ví dụ:

  • status = success nghĩa là đã thu tiền hay chỉ tạo payment URL?
  • amount là VND hay cents?
  • timeout có nghĩa là payment thất bại hay chưa biết?
  • Gọi lại cùng request có tạo payment mới không?

API tốt phải giảm hiểu nhầm.

---

17.9. REST, RPC, GraphQL: đừng bị tên làm rối

Có nhiều kiểu API:

  • REST.
  • RPC.
  • GraphQL.
  • gRPC.

Ở chương này, ta chưa cần đi sâu công nghệ.

Quan trọng là hiểu bản chất:

> Một bên gọi trực tiếp một bên khác và chờ kết quả.

REST thường xoay quanh resource:

GET /orders/{id}
POST /orders
POST /orders/{id}/cancel

RPC thường xoay quanh hành động:

CancelOrder
CreatePayment
RunGradingJob

GraphQL cho phép client chọn dữ liệu cần lấy:

query {
  order(id: "123") {
    id
    status
    items { name quantity }
  }
}

gRPC thường dùng trong service-to-service hiệu năng cao, schema rõ.

Nhưng dù dùng kiểu nào, các vấn đề vẫn giống:

  • Timeout.
  • Retry.
  • Contract.
  • Versioning.
  • Authentication.
  • Error handling.
  • Coupling.
  • Observability.

Đừng nghĩ đổi REST sang gRPC là tự động giải quyết sai boundary.

---

17.10. HTTP method: ý nghĩa thực dụng

Một API dễ hiểu nên dùng method có ý nghĩa.

GET     lấy dữ liệu
POST    tạo mới hoặc thực hiện hành động
PUT     thay thế toàn bộ resource
PATCH   sửa một phần
DELETE  xóa

Ví dụ:

GET /orders/123
POST /orders
POST /orders/123/cancel
PATCH /users/me
DELETE /cart/items/456

Đừng quá giáo điều.

Trong nghiệp vụ, nhiều hành động nên là POST:

POST /orders/123/cancel
POST /payments/456/refund
POST /grading-jobs/789/retry

Vì đây là command có luật nghiệp vụ, không chỉ update field.

API tốt nên diễn đạt ý định:

cancel order
refund payment
retry grading job

thay vì chỉ:

PATCH /orders/123 { "status": "cancelled" }

Set status trực tiếp thường làm mất luật domain.

---

17.11. Status code: đừng dùng 200 cho mọi thứ

HTTP status code giúp bên gọi hiểu kết quả.

Một vài mã phổ biến:

200 OK              thành công
201 Created         tạo mới thành công
202 Accepted        đã nhận, xử lý sau
204 No Content      thành công, không có body
400 Bad Request     input sai
401 Unauthorized    chưa đăng nhập/token sai
403 Forbidden       không có quyền
404 Not Found       không tìm thấy
409 Conflict        xung đột trạng thái
422 Unprocessable   dữ liệu hợp lệ về format nhưng sai nghiệp vụ/input
429 Too Many        quá giới hạn
500 Internal Error  lỗi server
503 Unavailable     service tạm không sẵn sàng

Ví dụ:

Nộp bài và tạo job chấm nền:

POST /submissions
-> 202 Accepted
{
  "submission_id": "sub_123",
  "grading_job_id": "job_456",
  "status": "pending"
}

Hủy đơn đã giao:

POST /orders/123/cancel
-> 409 Conflict
{
  "code": "ORDER_ALREADY_DELIVERED",
  "message": "Order cannot be cancelled after delivery"
}

Đừng trả 200 OK với body:

{ "success": false }

cho mọi lỗi.

Nó làm client khó xử lý và monitoring khó đọc.

---

17.12. Timeout là bắt buộc

Khi service A gọi service B, phải có timeout.

Không có timeout nghĩa là A có thể chờ mãi.

Trong hệ thống thật, chờ mãi là rất nguy hiểm:

  • Worker bị treo.
  • Connection bị giữ.
  • Thread/process bị chiếm.
  • Queue backlog tăng.
  • User chờ lâu.
  • Lỗi lan sang service khác.

Timeout là cách nói:

> Tôi chỉ chờ anh trong khoảng thời gian này. Quá thời gian đó, tôi sẽ tự quyết định bước tiếp theo.

Ví dụ:

Catalog API: timeout 300ms
Payment API: timeout 2s
AI Judge API: timeout 120s nếu chạy trong worker
Email provider: timeout 5s

Timeout phụ thuộc vào use case.

Không có con số chung cho mọi thứ.

Nhưng luôn phải có.

---

17.13. Timeout không có nghĩa là thất bại chắc chắn

Đây là một điểm rất quan trọng.

Khi bạn gọi Payment API và bị timeout, bạn không biết:

  • Payment chưa nhận request.
  • Payment nhận rồi nhưng chưa xử lý.
  • Payment xử lý thành công nhưng response bị mất.
  • Payment xử lý thất bại.

Timeout nghĩa là:

> Bên gọi không biết kết quả.

Không phải:

> Bên được gọi chắc chắn thất bại.

Vì vậy với các thao tác có side effect như:

  • Tạo payment.
  • Trừ tiền.
  • Tạo order.
  • Hoàn tiền.
  • Chạy grading job.

retry bừa sau timeout có thể tạo trùng.

Cần idempotency key hoặc cơ chế kiểm tra trạng thái.

Ví dụ:

POST /payments
Idempotency-Key: order_123_payment

Nếu request bị retry, Payment biết đây là cùng một yêu cầu, không tạo payment mới.

---

17.14. Retry: thuốc tốt nhưng dùng sai rất độc

Retry giúp xử lý lỗi tạm thời.

Ví dụ:

  • Network chập chờn.
  • Service đang restart.
  • Provider trả 503.
  • Timeout ngắn do tải đột biến.

Nhưng retry cũng có thể làm hệ thống sập nhanh hơn.

Giả sử Payment đang chậm vì quá tải.

Ordering timeout và retry 3 lần.

Mỗi request user giờ tạo thành 4 request đến Payment.

Payment càng quá tải hơn.

Đây là retry storm.

Retry nên có:

  • Số lần giới hạn.
  • Backoff.
  • Jitter.
  • Timeout rõ.
  • Chỉ retry lỗi phù hợp.
  • Idempotency cho request có side effect.

Không nên retry mọi lỗi.

Ví dụ:

  • 400 Bad Request: không retry.
  • 401 Unauthorized: không retry bừa.
  • 403 Forbidden: không retry.
  • 404 Not Found: thường không retry.
  • 409 Conflict: tùy nghiệp vụ.
  • 429 Too Many Requests: retry sau theo Retry-After.
  • 500/503/timeout: có thể retry có kiểm soát.

---

17.15. Idempotency với HTTP API

Idempotency nghĩa là:

> Gửi cùng một yêu cầu nhiều lần vẫn chỉ tạo ra một kết quả như một lần.

Với HTTP API, idempotency rất quan trọng cho command có side effect.

Ví dụ:

POST /payments
Idempotency-Key: checkout_abc_123

Lần đầu:

Payment tạo payment_id = pay_1

Lần retry cùng key:

Payment trả lại pay_1

Không tạo pay_2.

Idempotency cần cho:

  • Tạo payment.
  • Hoàn tiền.
  • Tạo order.
  • Tạo grading job.
  • Trừ quota.
  • Gửi webhook xử lý trạng thái.

Không phải API nào cũng cần idempotency key.

Nhưng API có side effect quan trọng thì nên nghĩ đến.

---

17.16. Circuit breaker là gì?

Circuit breaker là cơ chế ngắt tạm thời khi service phía sau đang lỗi nhiều.

Nói dễ hiểu:

> Nếu thấy gọi B liên tục lỗi, A tạm ngừng gọi B một lúc để tránh làm mọi thứ tệ hơn.

Ví dụ:

Ordering gọi Recommendation
Recommendation đang down

Nếu không có circuit breaker, mỗi request order vẫn cố gọi Recommendation, timeout, chậm.

Nếu có circuit breaker:

Sau nhiều lỗi, tạm bỏ qua Recommendation trong 30 giây
Trả response chính nhanh hơn
Sau đó thử lại

Circuit breaker phù hợp với dependency không bắt buộc hoặc có fallback.

Ví dụ:

  • Recommendation.
  • Analytics.
  • Non-critical profile enrichment.
  • External CRM.

Nhưng với dependency bắt buộc như Payment trong checkout, circuit breaker vẫn hữu ích để fail fast:

Payment đang unavailable
-> trả lỗi rõ ngay
-> không để user chờ timeout dài

---

17.17. Fallback: nếu API phụ không trả lời thì sao?

Fallback là phương án dự phòng khi dependency lỗi.

Ví dụ:

Catalog gọi Recommendation để lấy sản phẩm gợi ý.

Nếu Recommendation lỗi:

Hiển thị sản phẩm bán chạy thay thế

Ví dụ:

Profile service chậm:

Hiển thị tên cơ bản từ cache

Ví dụ:

Exchange rate API lỗi:

Dùng tỷ giá cache gần nhất, nếu nghiệp vụ cho phép

Không phải chỗ nào cũng có fallback.

Payment lỗi thì không thể giả vờ thanh toán thành công.

AI Judge lỗi thì không thể bịa điểm.

Nhưng nhiều dependency phụ có thể degrade gracefully.

Tức là hệ thống vẫn hoạt động ở mức giảm chức năng.

---

17.18. API Gateway nằm ở đâu trong câu chuyện này?

API Gateway là cửa vào chung cho client hoặc service bên ngoài.

Ví dụ:

Frontend -> API Gateway -> services

Gateway có thể xử lý:

  • Routing.
  • Authentication.
  • Rate limiting.
  • Request logging.
  • TLS termination.
  • Response transformation nhẹ.
  • Aggregation đơn giản.

Nhưng Gateway không nên chứa quá nhiều nghiệp vụ.

Nếu Gateway bắt đầu quyết định:

  • Đơn có được hủy không.
  • Payment có được refund không.
  • Học viên có được qua bài không.

thì nghiệp vụ bị đặt sai chỗ.

Gateway là cửa.

Use case/service bên trong mới là nơi xử lý luật nghiệp vụ.

---

17.19. Backend for Frontend là gì?

Backend for Frontend, thường gọi là BFF, là backend phục vụ riêng cho một loại frontend.

Ví dụ:

Mobile App -> Mobile BFF
Web App -> Web BFF
Admin -> Admin BFF

BFF hữu ích khi mỗi frontend cần dữ liệu khác nhau.

Ví dụ trang chi tiết đơn hàng trên mobile cần:

  • Trạng thái đơn.
  • Tóm tắt item.
  • Nút thao tác.

Admin cần:

  • Lịch sử payment.
  • Delivery log.
  • Support note.
  • Audit trail.

Nếu ép tất cả dùng cùng một API response khổng lồ, sẽ khó tối ưu.

BFF có thể gọi nhiều service bên dưới và gom dữ liệu cho UI.

Nhưng cần cẩn thận:

> BFF nên phục vụ trải nghiệm frontend, không nên trở thành nơi chứa nghiệp vụ cốt lõi.

---

17.20. API aggregation: gọi nhiều service để dựng một màn hình

Một màn hình thường cần dữ liệu từ nhiều nơi.

Ví dụ admin xem đơn:

Order info từ Ordering
Payment info từ Payment
Delivery info từ Delivery
Support notes từ Support

Có vài cách:

Cách 1: Frontend gọi nhiều API

Frontend -> Ordering
Frontend -> Payment
Frontend -> Delivery
Frontend -> Support

Đơn giản nhưng frontend biết quá nhiều backend.

Cách 2: BFF/API gọi gom

Frontend -> Admin BFF
Admin BFF -> Ordering/Payment/Delivery/Support

Frontend đơn giản hơn, nhưng BFF phải xử lý timeout/fallback.

Cách 3: Read model tổng hợp

Event từ nhiều service
-> build OrderAdminView
Frontend -> GET /admin/orders/{id}

Phức tạp hơn nhưng đọc nhanh, ít gọi dây chuyền.

Không có cách duy nhất.

Nếu màn hình ít dùng và dữ liệu cần realtime, BFF gọi gom có thể ổn.

Nếu màn hình dùng nhiều, dữ liệu phức tạp, gọi nhiều service gây chậm, read model có thể tốt hơn.

---

17.21. API versioning

API đã có người dùng thì không thể đổi tùy tiện.

Ví dụ response cũ:

{
  "order_id": "123",
  "total": 350000
}

Client đang dùng field total.

Nếu server đổi thành:

{
  "order_id": "123",
  "total_amount": 350000
}

client cũ có thể vỡ.

Cách xử lý:

  • Thêm field mới trước, giữ field cũ một thời gian.
  • Version endpoint nếu thay đổi lớn.
  • Version bằng path: /v1/orders.
  • Version bằng header.
  • Có deprecation plan.
  • Có contract test nếu nhiều service phụ thuộc.

Trong nội bộ công ty, nhiều người hay xem API nội bộ như "muốn đổi là đổi".

Nhưng nếu nhiều service dùng API đó, nó cũng là contract thật.

---

17.22. Error response nên rõ

Error tốt giúp client xử lý đúng.

Ví dụ:

{
  "code": "ORDER_CANNOT_BE_CANCELLED",
  "message": "Order cannot be cancelled after delivery",
  "details": {
    "order_id": "order_123",
    "current_status": "DELIVERED"
  }
}

Code máy đọc được:

ORDER_CANNOT_BE_CANCELLED

Message người đọc được:

Order cannot be cancelled after delivery

Details bổ sung để debug.

Đừng chỉ trả:

{
  "error": "Something went wrong"
}

cho lỗi nghiệp vụ mà client cần hiểu.

Nhưng cũng đừng lộ thông tin nhạy cảm:

  • Stack trace.
  • SQL query.
  • Secret.
  • Token.
  • Nội dung lỗi nội bộ không cần thiết.

---

17.23. Authentication và authorization

HTTP/API luôn cần nghĩ đến:

  • Ai đang gọi?
  • Có quyền làm việc này không?

Authentication trả lời:

> Người gọi là ai?

Authorization trả lời:

> Người đó có được làm việc này không?

Ví dụ:

GET /orders/123

Token hợp lệ chưa đủ.

Phải kiểm tra user có quyền xem order này không.

Trong service-to-service, cũng cần auth:

Ordering gọi Payment

Payment nên biết request đến từ service được phép, không phải ai đó giả mạo.

Các cách thường gặp:

  • JWT.
  • Session cookie.
  • API key.
  • OAuth2.
  • mTLS.
  • Service account token.

Chọn cách nào tùy hệ thống.

Điểm chính:

> API không nên mặc định tin mọi request chỉ vì nó nằm trong mạng nội bộ.

---

17.24. Rate limiting

Rate limiting giới hạn số request trong một khoảng thời gian.

Ví dụ:

100 requests/phút/user
1000 requests/phút/service

Rate limiting giúp:

  • Chống abuse.
  • Bảo vệ service khi client lỗi.
  • Giữ công bằng tài nguyên.
  • Tránh một bên kéo sập toàn hệ thống.

Ví dụ AI Judge:

Nếu một user hoặc một lớp gửi quá nhiều bài chấm cùng lúc, hệ thống có thể:

  • Giới hạn request tạo job.
  • Cho job vào queue.
  • Trả 429 Too Many Requests.
  • Hiển thị "đang có nhiều bài chờ chấm".

Rate limiting không chỉ dành cho public API.

Internal API cũng có thể cần giới hạn để tránh service này làm quá tải service khác.

---

17.25. Pagination, filtering, sorting

API đọc danh sách cần thiết kế cẩn thận.

Không nên:

GET /orders
-> trả toàn bộ 5 triệu đơn

Cần pagination:

GET /orders?limit=50&cursor=abc

Hoặc:

GET /orders?page=2&page_size=50

Với dữ liệu lớn và thay đổi liên tục, cursor pagination thường ổn định hơn page number.

Filtering:

GET /orders?status=paid&created_from=2026-05-01

Sorting:

GET /orders?sort=-created_at

API đọc tốt phải nghĩ đến performance từ đầu.

Danh sách không phân trang là một lỗi kinh điển.

---

17.26. API và cache

HTTP GET có thể cache.

Ví dụ:

  • Danh sách sản phẩm.
  • Chi tiết bài viết.
  • Cấu hình public.
  • Asset metadata.

Cache giúp giảm tải và giảm latency.

Nhưng cache cũng tạo câu hỏi:

  • Cache bao lâu?
  • Khi dữ liệu đổi thì invalidate thế nào?
  • Dữ liệu có được stale không?
  • Có dữ liệu cá nhân không?

Không nên cache bừa response chứa dữ liệu riêng tư.

Ví dụ:

GET /users/me

cần rất cẩn thận.

Với dữ liệu public:

GET /products
GET /articles

cache có thể rất hữu ích.

Cache là một chương riêng sau này, nhưng khi thiết kế HTTP API, phải biết endpoint nào có thể cache, endpoint nào không.

---

17.27. Observability cho HTTP call

Khi service gọi nhau, cần quan sát được:

  • Request rate.
  • Latency.
  • Error rate.
  • Timeout rate.
  • Status code.
  • Dependency nào chậm.
  • Trace của một request qua nhiều service.

Ví dụ user báo checkout chậm.

Ta cần biết:

Request đi qua service nào?
Mỗi service mất bao lâu?
Call nào timeout?
Database nào chậm?
Correlation id là gì?

Các công cụ:

  • Structured log.
  • Metrics.
  • Distributed tracing.
  • Correlation ID.
  • Dashboard.
  • Alert.

HTTP direct call dễ debug hơn event ở mức đơn giản.

Nhưng khi số service tăng, nếu không có trace, nó cũng rất khó hiểu.

---

17.28. Correlation ID

Correlation ID là một id gắn với một luồng request.

Ví dụ:

X-Correlation-ID: req_abc_123

Frontend gọi API.

API gọi Ordering.

Ordering gọi Payment.

Payment gọi provider.

Tất cả log cùng correlation_id.

Khi debug, ta tìm một id là thấy cả luồng.

Nếu không có correlation id, log của nhiều service sẽ rời rạc.

Trong hệ thống nhiều service, correlation id là thứ nhỏ nhưng rất đáng giá.

---

17.29. Gọi service nội bộ có cần OpenAPI/Schema không?

Có.

Nếu API là contract, schema giúp contract rõ.

OpenAPI có thể mô tả:

  • Endpoint.
  • Request.
  • Response.
  • Error.
  • Auth.

Với gRPC, protobuf đóng vai trò schema.

Schema giúp:

  • Tạo documentation.
  • Generate client.
  • Contract test.
  • Giảm hiểu nhầm.
  • Review thay đổi API dễ hơn.

Không phải lúc nào cũng cần cực kỳ formal từ ngày đầu.

Nhưng service-to-service mà không có contract rõ sẽ rất dễ vỡ khi team lớn.

---

17.30. Contract test

Contract test kiểm tra rằng API producer và consumer vẫn hiểu nhau.

Ví dụ Payment service hứa:

POST /payments
-> response có payment_id, status, payment_url

Ordering phụ thuộc vào các field này.

Nếu Payment đổi field payment_url thành redirect_url, contract test nên phát hiện trước khi deploy.

Contract test đặc biệt hữu ích khi:

  • Nhiều service do nhiều team làm.
  • API thay đổi thường xuyên.
  • Deploy độc lập.
  • Consumer không luôn được test cùng producer.

Không có contract test, lỗi thường xuất hiện ở runtime.

---

17.31. API nội bộ và API public khác nhau thế nào?

API public phục vụ bên ngoài công ty:

  • Mobile app.
  • Frontend public.
  • Partner.
  • Developer bên thứ ba.

API nội bộ phục vụ service/team bên trong.

API public cần ổn định hơn, document kỹ hơn, bảo mật kỹ hơn, versioning chặt hơn.

API nội bộ có thể đổi nhanh hơn, nhưng không có nghĩa là tùy tiện.

Nếu API nội bộ có nhiều consumer, nó cũng cần:

  • Contract rõ.
  • Deprecation plan.
  • Monitoring.
  • Versioning khi cần.

Câu hỏi thực dụng:

> Nếu đổi API này, ai có thể vỡ?

Nếu câu trả lời là "không chắc", bạn cần quản lý contract tốt hơn.

---

17.32. Gọi API bên ngoài

Gọi API bên ngoài khác gọi service nội bộ.

Ví dụ:

  • Gemini API.
  • Payment gateway.
  • Email provider.
  • SMS provider.
  • Delivery provider.

Bạn không kiểm soát được:

  • Uptime.
  • Latency.
  • Rate limit.
  • Schema thay đổi.
  • Lỗi tạm thời.
  • Chính sách retry.

Vì vậy cần:

  • Timeout.
  • Retry có kiểm soát.
  • Circuit breaker.
  • Rate limit phía mình.
  • Idempotency nếu có side effect.
  • Lưu raw response/webhook khi cần debug.
  • Adapter/client riêng, không gọi rải rác.

Ví dụ:

AiJudgeClient
PaymentGatewayClient
EmailProviderClient

Không nên để mọi nơi trong code tự gọi API bên ngoài.

Nếu provider đổi, chỉ nên sửa adapter.

---

17.33. AI Judge: HTTP chỗ nào, queue chỗ nào?

Bài toán AI Judge là ví dụ rất tốt.

Nếu mỗi bài chấm mất khoảng 1 phút 30 giây, không nên để request nộp bài chờ chấm xong.

Luồng tốt hơn:

Frontend -> POST /submissions
Backend:
  lưu submission
  tạo grading_job
  enqueue job
  trả 202 Accepted + job_id

Worker:
  lấy job
  gọi Gemini API qua HTTP
  lưu kết quả
  phát GradingCompleted

Frontend:
  GET /grading-jobs/{job_id}
  hoặc nhận notification khi xong

Ở đây HTTP dùng cho:

  • Frontend gửi bài.
  • Frontend xem trạng thái.
  • Worker gọi Gemini API.

Queue dùng cho:

  • Việc chấm lâu.
  • Kiểm soát concurrency.
  • Retry.
  • Không giữ request người dùng.

Điểm quan trọng:

> HTTP không biến mất. Nó chỉ không nên ôm phần xử lý lâu trong request chính.

---

17.34. Concurrency và HTTP call

Khi một service gọi HTTP ra ngoài, mỗi request đang chờ sẽ chiếm tài nguyên.

Tùy runtime, nó có thể chiếm:

  • Thread.
  • Process.
  • Connection.
  • Coroutine.
  • Worker slot.

Nếu bạn có 4 Celery workers và mỗi job chờ Gemini 90 giây, cùng lúc chỉ có 4 bài đang được chấm.

Không phải vì Gemini chỉ cho 4 request.

Mà vì phía bạn chỉ cho 4 job chạy đồng thời.

HTTP call lâu làm lộ rõ vấn đề concurrency.

Cách xử lý có thể là:

  • Tăng Celery concurrency phù hợp.
  • Dùng worker kiểu async/gevent nếu workload chủ yếu là I/O.
  • Tách AI Judge worker pool riêng.
  • Rate limit để không vượt quota/cost.
  • Theo dõi timeout và backlog.

Nhưng dù dùng cách nào, vẫn cần thiết kế job/use case rõ.

Đừng chỉ tăng số worker mà không đo bottleneck.

---

17.35. Sync API và async API

Một endpoint có thể trả kết quả ngay hoặc trả rằng việc đã được nhận.

Sync API

POST /orders
-> 201 Created
-> order đã tạo

Phù hợp khi việc ngắn và kết quả cần ngay.

Async API

POST /reports
-> 202 Accepted
-> report_job_id

Sau đó client kiểm tra:

GET /report-jobs/{id}

Phù hợp khi việc lâu.

AI Judge cũng vậy:

POST /submissions
-> 202 Accepted
-> grading_job_id

Không phải endpoint nào cũng cần trả kết quả cuối cùng ngay.

Đôi khi trả "đã nhận việc" là thiết kế đúng hơn.

---

17.36. Webhook có phải HTTP/API không?

Webhook cũng dùng HTTP, nhưng hướng gọi khác.

API thường là:

Hệ thống của ta gọi hệ thống khác

Webhook là:

Hệ thống khác gọi lại hệ thống của ta khi có sự kiện

Ví dụ Payment:

Payment Gateway -> POST /webhooks/payment

Webhook là chủ đề riêng ở chương sau, nhưng cần hiểu:

  • Nó vẫn là HTTP.
  • Nó thường bất đồng bộ theo nghĩa nghiệp vụ.
  • Nó cần verify signature.
  • Nó có thể gửi trùng.
  • Nó cần idempotency.
  • Nó cần trả response nhanh.

Webhook không nên xử lý mọi thứ nặng ngay trong request.

Nên nhận, xác thực, lưu event, rồi xử lý sau nếu cần.

---

17.37. HTTP API và database transaction

Không nên giữ database transaction trong lúc gọi HTTP API ngoài.

Ví dụ xấu:

begin transaction
  create order
  call payment gateway
  update order
commit

Nếu payment gateway chậm 5 giây, transaction giữ lock 5 giây.

Nếu gateway timeout, trạng thái khó xử lý.

Cách tốt hơn thường là:

begin transaction
  create order
  save OrderPlaced / PaymentRequested
commit

worker hoặc flow tiếp theo gọi payment

Hoặc nếu cần gọi payment ngay để trả payment URL:

create order transaction ngắn
commit
call payment gateway với idempotency key
save payment result transaction ngắn

Điểm chính:

> Gọi mạng là việc không chắc chắn. Đừng ôm lock database trong khi chờ mạng.

---

17.38. Security: đừng tin input từ API

Mọi input từ HTTP đều phải được xem là không đáng tin cho đến khi kiểm tra.

Cần kiểm tra:

  • Authentication.
  • Authorization.
  • Input validation.
  • Rate limit.
  • Payload size.
  • Content type.
  • Signature nếu là webhook.

Ví dụ:

POST /orders/123/cancel

Không được chỉ vì client gửi user_id trong body mà tin.

Server phải lấy actor từ token/session và kiểm tra quyền.

Ví dụ:

{
  "user_id": "admin"
}

Không có nghĩa request này là admin.

API tốt không để client tự tuyên bố quyền.

---

17.39. API response không nên lộ model nội bộ quá nhiều

Nếu response trả y nguyên database model, client sẽ phụ thuộc vào chi tiết nội bộ.

Ví dụ:

{
  "id": 123,
  "payment_status_internal": "CAPTURED_V2",
  "kitchen_flag": 7,
  "deleted_at": null,
  "retry_count": 3,
  "raw_gateway_payload": {...}
}

Client không cần biết hết.

Response nên phù hợp với use case:

{
  "order_id": "order_123",
  "status": "paid",
  "display_status": "Đã thanh toán",
  "total_amount": 350000
}

API là biên giới.

Biên giới tốt giúp bên trong thay đổi mà bên ngoài không vỡ.

---

17.40. Đừng dùng API để chia hệ thống khi boundary chưa rõ

Một lỗi rất phổ biến:

> Code đang rối, tách thành service và cho chúng gọi HTTP với nhau.

Nếu boundary nghiệp vụ chưa rõ, HTTP chỉ làm cái rối trở nên phân tán hơn.

Trước khi tách service, hãy hỏi:

  • Bounded context đã rõ chưa?
  • Service nào sở hữu dữ liệu nào?
  • API contract là gì?
  • Call nào đồng bộ, call nào async?
  • Nếu service kia down thì sao?
  • Có observability chưa?
  • Có retry/idempotency chưa?

Nếu chưa trả lời được, có thể nên tách module trong monolith trước.

HTTP giữa service là chi phí thật.

Đừng dùng nó chỉ để thay thế import function.

---

17.41. Checklist thiết kế HTTP/API

Khi thiết kế một API, hãy hỏi:

  • API này phục vụ use case gì?
  • Người gọi có cần kết quả ngay không?
  • Nếu xử lý lâu, có nên trả 202 Accepted và tạo job không?
  • Request schema là gì?
  • Response schema là gì?
  • Error format là gì?
  • Status code nào được dùng?
  • Timeout kỳ vọng là bao lâu?
  • Có retry không?
  • Retry có an toàn không?
  • Có cần idempotency key không?
  • Ai được gọi API này?
  • Quyền nghiệp vụ kiểm tra ở đâu?
  • API có thể cache không?
  • Có pagination không nếu trả list?
  • Có versioning không?
  • Có log/metrics/tracing không?
  • Nếu dependency phía sau down thì fallback hay fail?

Nếu không trả lời được, API đó có thể đang được thiết kế quá vội.

---

17.42. Bảng so sánh nhanh: HTTP/API với Queue/Event

| Câu hỏi | HTTP/API | Queue/Event | |---|---|---| | Bên gọi có chờ kết quả không? | Thường có | Thường không | | Phù hợp việc ngắn hay lâu? | Ngắn | Lâu hoặc xử lý sau | | Có coupling thời gian không? | Có | Ít hơn | | Debug ban đầu | Dễ hơn | Khó hơn | | Retry | Cần cẩn thận | Tự nhiên hơn nhưng vẫn cần idempotency | | Nhiều bên phản ứng | Không lý tưởng nếu gọi từng bên | Rất phù hợp | | Trả dữ liệu cho UI | Phù hợp | Không phù hợp trực tiếp | | Kiểm soát backlog | Không rõ bằng queue | Rõ hơn |

Không phải HTTP tốt hơn queue hay queue tốt hơn HTTP.

Chúng giải quyết nhu cầu khác nhau.

---

17.43. Bài tập: đọc một luồng API

Lấy một luồng trong hệ thống của bạn.

Ví dụ:

Nộp bài
Đặt hàng
Thanh toán
Hoàn tiền
Gửi thông báo
Tạo báo cáo

Hỏi:

1. Người dùng có cần kết quả cuối cùng ngay không? 2. API nào nên đồng bộ? 3. Việc nào nên chuyển sang queue? 4. Nếu service được gọi timeout thì sao? 5. Retry có tạo trùng dữ liệu không? 6. Có idempotency key chưa? 7. API error có rõ không? 8. Có call nào đang nằm trong transaction quá lâu không? 9. Có service nào bị gọi trên mọi request nhưng không thật sự bắt buộc không? 10. Có trace để biết request đi qua đâu không? 11. Có endpoint danh sách nào thiếu pagination không? 12. Có response nào lộ model nội bộ quá nhiều không?

Trả lời được các câu này, bạn sẽ thiết kế API thực tế hơn rất nhiều.

---

17.44. Tóm tắt bằng AI Judge

Với AI Judge:

Không nên:

Frontend
-> POST /submit
-> Backend gọi Gemini và chờ 90 giây
-> trả điểm

Vì:

  • Request lâu.
  • Dễ timeout.
  • Worker web bị chiếm.
  • User chờ.
  • Retry khó.
  • Khó kiểm soát concurrency.

Nên:

Frontend
-> POST /submissions
-> Backend tạo submission + grading_job
-> trả 202 Accepted + job_id

Worker
-> RunGradingJobUseCase
-> gọi Gemini API
-> lưu kết quả
-> phát GradingCompleted

Frontend
-> GET /grading-jobs/{id}
-> xem trạng thái/kết quả

HTTP vẫn dùng ở nhiều chỗ:

  • Client gửi bài.
  • Client xem kết quả.
  • Worker gọi Gemini.

Nhưng HTTP không còn ôm toàn bộ việc chấm trong request người dùng.

Đó là khác biệt lớn.

---

17.45. Kết luận của chương

HTTP/API là cách giao tiếp trực tiếp, dễ hiểu và cực kỳ quan trọng.

Nó phù hợp khi:

  • Cần request/response rõ ràng.
  • Cần kết quả ngay.
  • Thao tác đủ ngắn.
  • Truy vấn dữ liệu cho UI.
  • Quan hệ giữa caller và callee rõ.

Nhưng HTTP/API trở nên nguy hiểm khi:

  • Dùng cho việc lâu.
  • Gọi dây chuyền quá nhiều service.
  • Không có timeout.
  • Retry bừa.
  • Không có idempotency.
  • Contract mơ hồ.
  • Không có tracing/metrics.
  • Dùng để che boundary nghiệp vụ chưa rõ.

Thông điệp quan trọng nhất:

> HTTP/API là công cụ để hỏi và nhận câu trả lời ngay. Đừng dùng nó để bắt người dùng hoặc service phải chờ mọi việc phụ trong hệ thống hoàn tất.

Ở chương tiếp theo, ta sẽ đi sang Queue: cách giao việc rồi xử lý sau, rất quan trọng cho job nền, việc lâu, retry, backlog và kiểm soát concurrency.