This project implements a distributed transaction system using the Saga Orchestration Pattern, written in Go, and integrates multiple components such as:
- gRPC microservices (User, Product, Transaction)
- Payment Gateway (Midtrans)
- Messaging (NATS JetStream)
- Background processing (Asynq + Redis)
- Stateless Webhook Listener
This system allows users to:
- Create transactions
- Reserve product stock
- Initiate payments via Midtrans
- Handle async status updates
- Automatically expire or settle transactions
See full architecture flow in
saga-flow.png
Endpoint: Payload:
{
"products": [
{ "id": "product-123", "price": 10000, "quantity": 2 }
]
}Transaction ServicecallsUser Serviceusing gRPC.- User token is validated.
- If the user is not authorized, the process is stopped.
Transaction ServicecallsProduct Servicevia gRPC with:- Generated transaction ID
- Product IDs
- Requested prices
- Requested quantities
Product Service Validations:
- ❌ Product not found or mismatched → return error
- ❌ Stock is 0 → throw use-case error
- ❌ Requested quantity exceeds available → throw use-case error
- ❌ Price mismatch → throw use-case error
✅ If all validations pass:
- Stock is reserved (reduced)
- Reservation is saved as
ProductTransactionwith status:reserved - Returns success response to
Transaction Service
- After reservation is confirmed:
- Additional logic such as:
- Total price calculation
- Fee or discount processing
- Additional logic such as:
Transaction Servicesends HTTP request to Midtrans Snap API- On success, receives a Snap Token which allows user to proceed with payment
- Once Snap Token is obtained:
Transaction Servicepublishes acommittedevent to NATS JetStream
Example Payload:
{
"transaction_id": "TXN-123456"
}Product Consumer listens to transaction events from NATS JetStream.
It handles 4 types of events:
| Event | Description | Product Action |
|---|---|---|
committed |
User is ready to pay (Snap Token issued) | Update product transaction to committed |
cancelled |
Checkout failed or aborted | Mark as cancelled and restore stock |
expired |
Payment timeout | Mark as expired and restore stock |
settled |
User has successfully paid | Mark as settled and finalize stock |
- Every
committedtransaction schedules an expiration task using Asynq - Task runs after a TTL (e.g. 15 minutes)
- If the user hasn’t paid:
- Transaction is marked as
expired expiredevent is published to NATS JetStream
- Transaction is marked as
Two mechanisms run concurrently to check if the transaction has been paid:
- Midtrans sends webhook HTTP request to a stateless Listener Service
- Listener simply publishes the webhook data to
midtrans.payment_statusstream in NATS JetStream
- A scheduler runs periodically to check all
committedtransactions - It polls Midtrans API directly
- If payment is confirmed, transaction is marked as
settled
Transaction Workerconsumes events frommidtrans.payment_status(via NATS)- It calls business logic:
CheckAndUpdate(payload)- Validates the webhook data
- Updates transaction status to settled
- Emits settled event to NATS JetStream
- Product Consumer receives the
settledevent - Updates product transaction status to
settled - Stock is finalized and no longer reversible
| Phase | Mechanism | Technology Used |
|---|---|---|
| Auth & Validation | gRPC (sync) | Go, gRPC |
| Stock Reservation | gRPC + Logic | Go, gRPC |
| Payment Initialization | HTTP API | Midtrans Snap |
| Messaging | Event-driven | NATS JetStream |
| Expiry Enforcement | Delayed Task | Asynq (Redis) |
| Payment Confirmation | Webhook / Scheduler | Midtrans, Go, NATS |
| Final Update | Consumer Worker | Go Worker, Business Logic |
- Go 1.20+
- gRPC
- Redis + Asynq
- NATS JetStream
- Midtrans Snap API
- PostgreSQL
- Docker / Docker Compose (optional)
