Menu
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Đang tải...

Synchronous vs Asynchronous API patterns

API and Technical Fundamentals cho BA Bài 27/60

Mở đầu — vì sao bài này quan trọng

Hãy tưởng tượng bạn đang ngồi review một bản đặc tả tích hợp (integration spec) cho dự án thanh toán của một ví điện tử Việt Nam. Nhóm phát triển ghi: "Khi người dùng bấm Thanh toán, hệ thống gọi API ngân hàng và trả về kết quả ngay lập tức." Nghe có vẻ hợp lý, nhưng nếu bạn là một BA tinh ý, một câu hỏi phải bật lên trong đầu: Nếu ngân hàng mất 30 giây mới phản hồi thì sao? Người dùng có phải ngồi nhìn màn hình quay vòng vòng suốt 30 giây không? Nếu kết nối rớt giữa chừng thì tiền đã trừ hay chưa?

Toàn bộ những câu hỏi đó xoay quanh một quyết định kiến trúc cực kỳ căn bản nhưng thường bị xem nhẹ: API này nên là đồng bộ (synchronous) hay bất đồng bộ (asynchronous)? Đây không phải chuyện thuần kỹ thuật để dev tự quyết. Lựa chọn sync hay async ảnh hưởng trực tiếp đến trải nghiệm người dùng, đến cách bạn viết user story, đến màn hình "đang xử lý", đến email/SMS thông báo, và đến cách bạn định nghĩa trạng thái đơn hàng. BA hiểu rõ hai mô hình này sẽ viết được spec sát thực tế, đặt đúng câu hỏi cho team kỹ thuật, và tránh những lỗ hổng nghiệp vụ tốn kém.

Bài này tập trung chỉ vào sự khác biệt giữa hai mô hình giao tiếp này và cách chọn lựa giữa chúng. Các cơ chế cụ thể như webhook hay polling đã có bài riêng (Bài 26), còn các pattern message queue, event-driven sẽ được đào sâu ở các bài 39–40. Ở đây ta nắm chắc nền tảng tư duy.

Khái niệm cốt lõi

Synchronous (đồng bộ): gọi và đợi

Mô hình đồng bộ giống như bạn gọi điện cho tổng đài và giữ máy cho đến khi nhân viên trả lời xong câu hỏi của bạn. Client gửi request đi, rồi đứng yên chờ cho đến khi nhận được response. Chỉ khi có kết quả, client mới làm bước tiếp theo.

Client: POST /api/orders
        (đợi... 200ms...)
Server: 201 Created
        { "order_id": 8842, "status": "confirmed", "total": 450000 }
Client: nhận đủ dữ liệu → hiển thị "Đặt hàng thành công"

Điểm mấu chốt: trong suốt thời gian chờ, "đường dây" giữa client và server bị chiếm giữ (blocking). Kết quả trả về thường là kết quả cuối cùng — đơn hàng đã được tạo, dữ liệu đầy đủ nằm ngay trong response.

Ưu điểm:

  • Đơn giản, dễ hiểu, dễ lập trình và dễ debug. Một request đi, một response về, bạn biết ngay thành công hay thất bại.
  • Có kết quả tức thì để xử lý ngay — phù hợp khi người dùng cần thấy phản hồi lập tức.
  • Luồng nghiệp vụ tuyến tính, dễ viết user story: "Khi A xảy ra thì B, rồi C."
Nhược điểm:
  • Nếu xử lý lâu, người dùng phải chờ. Chờ quá 2–3 giây là trải nghiệm bắt đầu tệ.
  • Dễ bị timeout: nếu server xử lý quá lâu, kết nối bị ngắt và client không biết việc đã xong hay chưa.
  • Không chịu tải tốt: mỗi request đang chờ chiếm một "kết nối" của server. Nhiều người gọi cùng lúc dễ làm nghẽn hệ thống.

Asynchronous (bất đồng bộ): gọi, nhận biên nhận, rồi tính sau

Mô hình bất đồng bộ giống như bạn nộp hồ sơ ở văn phòng công chứng. Bạn không đứng đó chờ họ xử lý xong — bạn nhận một số phiếu hẹn rồi ra về, hôm sau quay lại lấy kết quả (hoặc họ gọi điện báo bạn). Client gửi request, server chấp nhận yêu cầu và trả về ngay một "biên nhận" (thường là mã 202 Accepted kèm một job ID hoặc reference). Việc xử lý thật diễn ra ở hậu trường. Client không bị chặn, có thể làm việc khác.

Client: POST /api/orders
Server: 202 Accepted
        { "job_id": "ord_xk29", "status": "processing",
          "status_url": "/api/orders/jobs/ord_xk29" }
Client: nhận biên nhận → hiển thị "Đơn hàng đang được xử lý"
... ít giây/phút sau ...
Server xử lý xong → thông báo lại cho client
        (qua webhook gọi ngược về, hoặc client tự hỏi lại — xem Bài 26)

Ưu điểm:

  • Xử lý được tác vụ nặng/lâu mà không bắt người dùng chờ: render video, đối soát giao dịch, gửi hàng loạt email, gọi sang đối tác chậm.
  • Chịu tải tốt hơn, "đệm" được những lúc tăng đột biến (peak) — request xếp hàng chờ xử lý dần thay vì làm sập hệ thống.
  • Tách rời (decouple) client và server: bên kia chậm hay tạm lỗi cũng không kéo sập bên này.
Nhược điểm:
  • Phức tạp hơn nhiều: phải thiết kế cơ chế theo dõi trạng thái, thông báo kết quả, xử lý khi job thất bại.
  • Trải nghiệm cần thiết kế kỹ: phải có màn hình "đang xử lý", thông báo khi xong, xử lý trường hợp người dùng đóng app giữa chừng.
  • Khó debug và khó kiểm thử hơn, vì kết quả không trả về ngay trong một lượt request-response.

Cách phân biệt nhanh trong spec

Một mẹo để nhận diện: hãy nhìn vào mã trạng thái và nội dung response dự kiến.

  • Nếu response là 200 OK hoặc 201 Created kèm dữ liệu kết quả cuối cùng → đó là API đồng bộ.
  • Nếu response là 202 Accepted kèm một ID để tra cứu sau, chưa có kết quả thật → đó là API bất đồng bộ.
Ngoài ra, đừng nhầm lẫn: "đồng bộ" không có nghĩa là "nhanh", và "bất đồng bộ" không có nghĩa là "chậm". Một API đồng bộ trả lời trong 100ms vẫn là đồng bộ. Một API bất đồng bộ có thể trả biên nhận trong 50ms nhưng việc thật mất 5 phút. Sự khác biệt nằm ở chỗ client có phải chờ kết quả cuối cùng ngay trong cùng một lượt gọi hay không.

Tình huống thực tế

Ví dụ 1: Tra cứu số dư ví điện tử — chọn đồng bộ

Một ví điện tử như MoMo có chức năng "Xem số dư". Khi người dùng mở app, app gọi GET /api/wallet/balance. Đây là tác vụ đọc dữ liệu, rất nhanh (thường dưới 100ms), và người dùng cần thấy con số ngay — không ai chấp nhận màn hình "Số dư của bạn đang được tính toán, vui lòng chờ".

Ở đây mô hình đồng bộ là lựa chọn hiển nhiên. Request đi, response về với 200 OK { "balance": 1250000, "currency": "VND" }, app hiển thị tức thì. Nếu lỡ có lỗi (500 chẳng hạn), app báo "Không tải được số dư, thử lại" — cũng tức thì.

Bài học rút ra: Với tác vụ đọc dữ liệu nhanh, đơn giản, người dùng cần kết quả ngay → mặc định chọn đồng bộ. Đừng làm phức tạp hóa bằng async khi không cần. Một sai lầm phổ biến của BA mới là nghĩ "async hiện đại hơn nên dùng async cho mọi thứ" — sai. Async chỉ trả giá phức tạp xứng đáng khi tác vụ thực sự nặng hoặc lâu.

Ví dụ 2: Xuất báo cáo doanh thu cuối tháng tại một sàn TMĐT — chọn bất đồng bộ

Một sàn thương mại điện tử như Tiki có nhà bán (seller) muốn "Xuất báo cáo doanh thu tháng" gồm hàng chục nghìn đơn hàng, tính toán phí, hoàn tiền, đối soát. Quá trình tổng hợp này có thể mất 2–4 phút. Nếu thiết kế đồng bộ, người bán bấm nút "Xuất báo cáo" rồi ngồi nhìn màn hình quay 3 phút — và gần như chắc chắn sẽ dính timeout vì hầu hết cấu hình mạng/server cắt kết nối sau 30–60 giây. Kết quả: trải nghiệm tệ và lỗi liên miên.

Giải pháp đúng là bất đồng bộ. Người bán bấm "Xuất báo cáo" → API trả về 202 Accepted { "report_id": "rpt_5521", "status": "generating" } và hiện thông báo "Báo cáo đang được tạo, chúng tôi sẽ gửi email cho bạn khi hoàn tất." Người bán có thể đóng trang, đi làm việc khác. Vài phút sau, hệ thống gửi email kèm link tải file, hoặc một thông báo trong app.

Bài học rút ra: Khi tác vụ nặng và có thể vượt quá vài giây, async cứu cả trải nghiệm lẫn độ ổn định. Là BA, bạn cần viết rõ trong spec: trạng thái "đang tạo" hiển thị thế nào, kết quả được thông báo qua kênh nào (email/in-app), file lưu bao lâu, và nếu tạo thất bại thì người dùng nhận thông báo gì. Đây chính là phần nghiệp vụ mà dev không tự nghĩ ra giúp bạn.

Ví dụ 3: Đặt vé xe khách liên tỉnh — sự lai (hybrid) tinh tế

Một nền tảng đặt vé như Vexere có luồng đặt vé phức tạp: giữ ghế, gọi sang cổng thanh toán (ví, thẻ, ngân hàng), rồi xác nhận vé. Bài toán thú vị ở chỗ: phần "giữ ghế" cần đồng bộ (người dùng cần biết ngay ghế còn trống hay không), nhưng phần "xác nhận thanh toán từ ngân hàng" lại thường bất đồng bộ — vì ngân hàng có thể mất vài giây tới vài chục giây, hoặc người dùng phải sang app ngân hàng nhập OTP rồi quay lại.

Cách thiết kế thực tế: bước giữ ghế đồng bộ (200 OK, ghế đã được giữ trong 10 phút). Bước thanh toán bất đồng bộ — hệ thống tạo giao dịch, hiển thị "Đang chờ xác nhận thanh toán", và lắng nghe phản hồi từ ngân hàng (cổng thanh toán gọi ngược về báo "thành công/thất bại"). Khi nhận được, hệ thống mới phát hành vé và gửi SMS/email vé điện tử.

Bài học rút ra: Một luồng nghiệp vụ thực tế thường trộn cả hai mô hình. Nhiệm vụ của BA là tách luồng thành từng bước và quyết định từng bước nên sync hay async, thay vì gắn một nhãn duy nhất cho cả luồng. Đặc biệt với thanh toán, gần như luôn có phần async, kéo theo các yêu cầu nghiệp vụ quan trọng: trạng thái "chờ xác nhận", thời gian timeout giữ ghế, và xử lý tình huống "tiền đã trừ nhưng chưa có vé".

Hướng dẫn từng bước

Khi review hoặc viết spec cho một API và cần quyết định sync hay async, hãy đi theo quy trình sau:

  • Xác định bản chất tác vụ. Đây là đọc dữ liệu đơn giản, hay ghi/xử lý phức tạp? Đọc nhanh → thiên về sync. Xử lý nặng → cân nhắc async.
  • Ước lượng thời gian xử lý. Hỏi team kỹ thuật: "Tác vụ này mất bao lâu ở trường hợp xấu nhất (worst case)?" Quy tắc ngón tay cái: dưới 1–2 giây thì sync ổn; trên 5–10 giây thì nên async; khoảng giữa thì cân nhắc thêm tải hệ thống.
  • Xét sự phụ thuộc vào bên thứ ba. API có gọi sang đối tác bên ngoài (ngân hàng, hãng vận chuyển, nhà cung cấp SMS) không? Bên thứ ba càng chậm hoặc càng không kiểm soát được → càng nên async để không bị họ kéo sập.
  • Đặt câu hỏi về trải nghiệm người dùng. Người dùng có cần kết quả ngay tại chỗ để làm bước tiếp theo không? Nếu có → ưu tiên sync. Nếu họ chấp nhận "để xong rồi báo lại" → async hoàn toàn được.
  • Nếu chọn async, định nghĩa đủ phần nghiệp vụ đi kèm. Đây là phần BA hay bỏ sót:
- Trạng thái trung gian hiển thị thế nào ("đang xử lý", "đang chờ xác nhận")? - Kết quả được thông báo qua kênh nào (webhook, email, SMS, in-app)? - Người dùng tra cứu lại trạng thái bằng cách nào? - Nếu xử lý thất bại thì sao? Có tự thử lại (retry) không? Báo cho ai? - Kết quả lưu trong bao lâu?

  • Viết rõ trong acceptance criteria. Ví dụ: "Hệ thống phản hồi 202 Accepted trong vòng 1 giây" và "Người dùng nhận email kết quả trong vòng 5 phút kể từ khi gửi yêu cầu."

Lỗi thường gặp & mẹo

Lỗi 1 — Lạm dụng đồng bộ cho tác vụ nặng. Bắt người dùng chờ một tác vụ 30 giây qua API đồng bộ là công thức gây timeout và bỏ giỏ hàng. Mẹo: nếu thấy spec mô tả "gọi API rồi đợi xử lý xong" mà tác vụ liên quan đến tính toán lớn, gửi hàng loạt, hay gọi bên thứ ba, hãy gắn cờ ngay để bàn lại với team.

Lỗi 2 — Dùng async nhưng quên thiết kế cách thông báo kết quả. Đây là lỗi BA kinh điển: viết spec "đơn hàng được xử lý bất đồng bộ" rồi dừng lại. Câu hỏi sống còn là: làm sao người dùng biết kết quả? Không có phần này, async chỉ là một cái hố đen. Luôn đi kèm cơ chế thông báo (xem Bài 26 về webhook vs polling).

Lỗi 3 — Nhầm "nhanh/chậm" với "sync/async". Như đã nói, đây là hai trục khác nhau. Một async có thể trả biên nhận cực nhanh. Phân biệt dựa trên "client có chờ kết quả cuối cùng trong cùng lượt gọi không", chứ không phải tốc độ.

Lỗi 4 — Quên trạng thái lỗi của tác vụ async. Với sync, lỗi trả về ngay (4xx/5xx). Với async, tác vụ có thể thất bại sau khi đã trả 202. Spec phải định nghĩa: trạng thái "failed" trông thế nào, người dùng được báo ra sao, có cho thử lại không.

Mẹo vàng: Khi không chắc, hãy hỏi đúng một câu với team kỹ thuật: "Tác vụ này ở trường hợp tệ nhất mất bao lâu, và có gọi sang hệ thống nào ngoài tầm kiểm soát của chúng ta không?" Câu trả lời gần như quyết định luôn nên sync hay async.

Bài tập thực hành

  • Phân loại. Với mỗi tác vụ sau, hãy quyết định nên thiết kế đồng bộ hay bất đồng bộ và giải thích ngắn gọn lý do (1–2 câu):
- (a) Đăng nhập bằng email + mật khẩu. - (b) Tải lên (upload) và xử lý một video dài 20 phút để phát trên nền tảng học trực tuyến. - (c) Kiểm tra mã giảm giá còn hiệu lực hay không. - (d) Gửi 50.000 email thông báo khuyến mãi cho toàn bộ khách hàng. - (e) Hoàn tiền một giao dịch qua cổng thanh toán ngân hàng.

  • Viết acceptance criteria. Cho chức năng "Xuất file Excel danh sách 80.000 khách hàng" của một CRM. Hãy chọn mô hình phù hợp và viết 4–5 tiêu chí chấp nhận (acceptance criteria), nhớ bao gồm: response ban đầu, cách hiển thị trạng thái, cách thông báo kết quả, và xử lý khi thất bại.
  • Soi spec. Một spec ghi: "Khi khách bấm Đặt hàng, hệ thống gọi API kho để trừ tồn, gọi API thanh toán, gọi API vận chuyển tạo đơn giao, rồi trả về 200 OK với mã vận đơn." Hãy chỉ ra ít nhất hai rủi ro của cách thiết kế hoàn toàn đồng bộ này, và đề xuất bước nào nên chuyển sang bất đồng bộ.
Gợi ý tự kiểm: với câu 1, (a) và (c) hợp đồng bộ; (b), (d) hợp bất đồng bộ; (e) thường bất đồng bộ vì phụ thuộc ngân hàng.

Tóm tắt

  • Đồng bộ (sync): client gọi và chờ kết quả cuối cùng trong cùng một lượt request-response. Đơn giản, tức thì, dễ debug — nhưng dễ timeout và không hợp tác vụ nặng. Dấu hiệu nhận biết: 200/201 kèm dữ liệu kết quả đầy đủ.
  • Bất đồng bộ (async): client gọi, nhận biên nhận (202 Accepted + ID tra cứu), xử lý diễn ra ở hậu trường, kết quả được thông báo sau. Chịu tải tốt, hợp tác vụ nặng/lâu/phụ thuộc bên thứ ba — nhưng phức tạp, cần thiết kế kỹ phần thông báo và xử lý lỗi.
  • Hai trục "sync/async" và "nhanh/chậm" khác nhau — đừng nhầm lẫn.
  • Quyết định dựa trên: bản chất tác vụ, thời gian xử lý worst-case, mức phụ thuộc bên thứ ba, và nhu cầu thấy kết quả ngay của người dùng.
  • Một luồng nghiệp vụ thực tế thường lai cả hai. Việc của BA là tách luồng và quyết từng bước.
  • Nếu chọn async, trách nhiệm của BA là định nghĩa đầy đủ: trạng thái trung gian, kênh thông báo kết quả, cách tra cứu, xử lý thất bại, và thời gian lưu kết quả.
Nắm vững mô hình này, bạn sẽ đặt được những câu hỏi sắc bén ngay từ khâu spec — và đó chính là khác biệt giữa một BA chỉ "chép yêu cầu" với một BA thực sự hiểu hệ thống.