Xin chào mọi người, mình là Hoàng Việt Software Intern vừa tham gia VNG. Khi mới vào tham gia dự án, mình bắt đầu từ một email liệt kê khá nhiều thứ từ anh mentor của mình: Go Gin, MySQL, MongoDB, Temporal, Keycloak, OIDC/OAuth, HTTP/1.1, HTTP/2 và gRPC. Nếu mở từng docs ra đọc riêng, mình có thể nhớ định nghĩa, nhưng rất khó hình dung khi chúng đứng chung trong một backend thật thì request sẽ đi qua đâu @@.
Vì vậy mình chọn dựng một playground nhỏ: một mini Order System. Mục tiêu không phải làm production-ready system, mà là có một flow chạy được để vừa build, vừa debug, vừa tự trả lời: công nghệ này xuất hiện ở bước nào và giải quyết vấn đề gì?
Để tránh bài viết quá dài, hôm nay mình sẽ không đi sâu vào phần code ra playground này, mà chỉ tập trung vào cách khai thác, đọc hiểu và nắm được luồng hoạt động của 3 công nghệ cốt lõi: Temporal, Keycloak và gRPC thôi nhen ^^. Toàn bộ bài viết sẽ nói về cách chạy dự án này, cách mình tư duy khi tiếp cận với công nghệ mới, và các sai sót mình đã gặp phải khi build nên có gì sai xót mọi người góp ý giúp mình phía bên dưới nhé!
Playground mình đã chuẩn bị: https://github.com/Keruedu/OrderPlayground
Prerequisites: cần gì trước khi chạy?
- Docker Desktop và Docker Compose để chạy local environment.
- PowerShell cơ bản để gọi API, lấy token và query nhanh.
- Go cơ bản: package, handler, context và cách app đọc env config.
- HTTP/JWT cơ bản: biết Bearer token được gửi qua header Authorization.
- Không cần biết sâu Temporal hay Keycloak trước. Bài này sẽ dùng flow order để giải thích vừa đủ.
What we build: mini Order Processing System
Flow chính rất nhỏ:
user login qua Keycloak, gọi API tạo order vào Gin gateway, gateway ghi order vào MySQL, ghi audit event vào MongoDB, rồi start Temporal workflow. Workflow sau đó gọi inventory-service và notifier-service qua gRPC, cuối cùng cập nhật trạng thái order thành COMPLETED hoặc FAILED.
Đây là cách mình tự nhớ vai trò từng thành phần là: Gin nhận request, Keycloak cấp token, MySQL giữ state chính, MongoDB giữ nhật ký, Temporal điều phối việc dài hơi, còn gRPC là đường nói chuyện nội bộ giữa service.
Architecture overview
Hình 1. Architecture tổng quan của playground.
Public API dùng HTTP/JSON vì dễ gọi bằng curl/Postman. Internal service dùng gRPC vì contract rõ hơn và chạy trên HTTP/2. Temporal không nằm trong request trực tiếp; nó nhận workflow sau khi gateway đã tạo order PENDING.
Mình cũng cố tình chia ranh giới giao thức cho dễ học. Từ user vào gateway là HTTP/1.1 JSON, vì đây là kiểu API quen thuộc nhất để test. Từ workflow sang inventory/notifier là gRPC, để thấy HTTP/2 xuất hiện trong giao tiếp nội bộ. Còn Keycloak dùng OIDC/OAuth2 để phát token, không phải nơi lưu order hay chạy workflow.
Decision & trade-off
| Decision | Vì sao chọn | Trade-off |
|---|---|---|
| Order System thay vì hello world | Một flow nhỏ nhưng có auth, DB, audit, workflow và internal service call. | Setup nặng hơn, nhưng bù lại thấy được mối nối thật giữa các tech. |
| MySQL cho order | Order và order_items có schema rõ, hợp relational database. | Phải để ý migration và connection pool. |
| MongoDB cho audit | Audit event linh hoạt metadata, dễ lưu dạng document. | Muốn query tốt vẫn phải nghĩ tới index. |
| HTTP/JSON public, gRPC internal | Client bên ngoài dễ test, service bên trong có contract chặt hơn. | gRPC debug khó hơn REST nếu thiếu tooling. |
| Temporal cho workflow | Có retry, history và UI để quan sát order đi qua từng bước. | Workflow code cần cẩn thận với versioning/determinism. |
Step-by-step: chạy playground
Phần này mình sẽ hướng dẫn step by step cách chạy playground này. Ngoài ra, trong quá trình build, mình cũng gặp khá nhiều lỗi cần debug. Phần này nội dung khá dài nhưng cũng rất quan trọng, nên mình sẽ tách riêng thành một bài viết khác nhé ^^. Ở bài viết đó mình sẽ nói về các bug mình gặp phải và cách mà mình khi giải quyết chúng.
Start Docker Compose
docker compose -f infra\docker\docker-compose.yml --env-file .env up -d --buildTrong lần test thật trên máy mình, port 27017 và 7233 đang bị container khác giữ, nên mình dùng override để Mongo expose ra 27018 và Temporal expose ra 7234. Đây là bài học local khá thực tế: trước khi nghi code sai, hãy kiểm tra port conflict.
$env:MONGO_PORT='27018'; docker compose -f infra\docker\docker-compose.yml -f infra\docker\docker-compose.local-ports.yml --env-file .env up -dHình 2. Các container/port chính trong playground.
Hình 3. Smoke test thật: một order COMPLETED và một order FAILED.
Login lấy token từ Keycloak
$userToken = (Invoke-RestMethod -Method Post `
-Uri "http://localhost:8081/realms/order-playground/protocol/openid-connect/token" `
-ContentType "application/x-www-form-urlencoded" `
-Body @{
client_id="gateway-api"
grant_type="password"
username="user1"
password="user1pass"
}).access_tokenỞ đây Keycloak giống nơi phát thẻ vào cửa. Gateway không tự login user, nó chỉ kiểm tra thẻ đó có đúng issuer, đúng audience và có role phù hợp không.
Hình 4. Keycloak: client gateway-api trong realm order-playground.
Gọi POST /api/orders
$body = @{
customer_name = "Nguyen Trung"
currency = "USD"
items = @(
@{ sku = "BOOK-001"; quantity = 1; price = 15.5 },
@{ sku = "PEN-002"; quantity = 2; price = 4.25 }
)
} | ConvertTo-Json -Depth 5
$order = Invoke-RestMethod -Method Post `
-Uri "http://localhost:8080/api/orders" `
-Headers @{ Authorization = "Bearer $userToken" } `
-ContentType "application/json" `
-Body $bodyHình 5. Sequence tạo order từ login đến workflow.
Kiểm tra MySQL, MongoDB và Temporal
docker exec order-playground-mysql mysql -uorder_app -porder_pass -D order_playground -e "SELECT id,status,created_by FROM orders;"
docker exec order-playground-mongodb mongosh --username mongoadmin --password mongopass --authenticationDatabase admin order_playground --quiet --eval "db.audit_events.find().pretty()"MySQL cho mình biết state hiện tại của order. MongoDB cho mình timeline nghiệp vụ. Temporal UI cho mình biết workflow fail ở bước nào, retry bao nhiêu lần, hay đã completed.
Một mẹo nhỏ khi test là đừng chỉ nhìn response của POST /api/orders. Response lúc đầu trả PENDING là đúng, vì workflow chạy async. Mình cần chờ vài giây rồi gọi GET /api/orders/:id hoặc mở Temporal UI để biết kết quả cuối cùng. Đây cũng là chỗ mình hiểu rõ hơn sự khác nhau giữa request lifecycle của Gin và workflow lifecycle của Temporal.
Hình 6. Temporal UI: workflow order đã COMPLETED.
Hình 7. Audit events để đối chiếu flow.
Testing: happy path và failure path
- Happy path: tạo order với quantity nhỏ hơn hoặc bằng 5, kỳ vọng order đi từ PENDING sang COMPLETED.
- Failure path: tạo order có quantity lớn hơn 5, inventory-service reject và workflow chuyển order sang FAILED.
- Auth path: gọi /api/orders không token để thấy 401, dùng user1 gọi /api/admin/orders để thấy 403.
Để cho thuận tiện mình đã viết trước file test chỉ cần chạy:
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\test.ps1File test cho cả 3 trường hợp mình để ở:
- Entry point: scripts/test.ps1
- Test logic: tests/e2e/order-scenarios.ps1
Cách mình đọc repo để không bị ngợp
Nếu đọc repo từ đầu đến cuối, mình rất dễ bị lạc vì có nhiều folder @@. Cách hợp lý hơn là đọc theo hành trình của một order. Mỗi lần đi qua một lớp, mình chỉ hỏi một câu: lớp này nhận gì, ghi gì, gọi ai tiếp theo?
- Đọc router/handler trước để biết request vào đâu.
- Lần theo repository MySQL/MongoDB để biết dữ liệu được ghi ở đâu.
- Mở workflow/activity sau cùng để hiểu phần async.
- Khi lỗi auth thì mở Keycloak; khi order kẹt thì mở Temporal; khi state sai thì mở DB.
Tổng kết
Điểm mình thích ở cách học này là không phải nhồi nhét tất cả các lý thuyết cùng lúc. Nó cho mình một order thật để lần theo. Từ đó, mỗi thuật ngữ không còn đứng riêng: Keycloak nằm ở cửa vào, MySQL/MongoDB nằm ở state/audit, Temporal nằm ở workflow, còn gRPC nằm ở service nội bộ.
Bài học lớn nhất của mình: với một stack rộng, một playground nhỏ nhưng chạy thật giúp học nhanh hơn nhiều so với đọc từng tech riêng lẻ.
Ngoài ra trong quá trình build mình cũng gặp rất nhiều debug mình sẽ viết và tách bài debug ra riêng nhé vì phần nội dung đó cũng khá dài nhưng lại quan trọng ^^






