[ Go ] 1 of 6
Payment System
Idempotent Go payment lifecycle: order, charge, HMAC callbacks, outbox, refunds.
cd project && docker compose up --build
[ 01 ] WHAT IT IS
In plain words
A working payment lifecycle built end to end: a customer signs in, picks a product, places an order, charges it, and refunds it. Every state change runs through one TransactionLifecycle module that pairs a PostgreSQL transaction with an outbox row and an idempotency key, so retries and duplicate clicks cannot double-charge.
It exists as a reference implementation of the unglamorous parts of payments: HMAC-verified partner callbacks with a 300s replay window, background workers that turn outbox rows into invoice files, and a dead-letter queue for poison messages.
It is aimed at backend engineers who want to read real code for retries, callbacks and reconciliation rather than a slide deck about them.
[ 02 ] ARCHITECTURE
How it is put together
- monolith/ - a single Go binary holding the TransactionLifecycle deep module: monolith/lifecycle/lifecycle.go for the state chart (CREATED to READY to SUCCESS/FAILED/REFUND) plus idempotency, and pricing.go for the discount math.
- monolith/cmd/ - wiring and the endpoint table: routes.go, handlers.go for the new API, legacy.go for the ms-* compatible paths, http.go for envelopes, partner and outbox transport.
- monolith/store/ - the persistence seam: store.go types with pg.go for production and memory.go for the dev/test fake, over PG transaction + store schemas.
- monolith/jobs/ - the background half: outbox.go polls every 5s and routes rows (ms-notify-payment to an invoice, servicelogs kept queryable) with 3x backoff then a terminal dlq.* row; sweeper.go moves stale READY to FAILED after 5m and purges servicelogs past LOG_RETENTION_DAYS. The invoice worker (monolith/invoice/ + invoiceworker/) is the consumer: it writes receipt files to INVOICE_DIR/invoice_<tx>.json with the transaction id sanitized against path traversal.
- ms-paymentagr/ - the partner stub (Go/Gin + Redis): charge, refund, a CheckoutUrl redirect page and HMAC-SHA256 signed callbacks, gated behind the api-key header and driven by ms-paymentagr/usecase/.
- frontend/ - Next.js 16 + TypeScript + Tailwind portal (login, catalog, checkout, status, refunds) whose server-side client lives in frontend/src/lib/gateway.ts.
[ 03 ] INSTALL
Set it up
cd project
cp .env.example .env
# edit .env and set every secret (PARTNER_API_KEY, NOTIFY_SECRET, POSTGRES_PASSWORD, ...)
docker compose up --build
[ 04 ] QUICKSTART
See it work
- Wait for the five containers (postgres, redis, ms-paymentagr, monolith, frontend) to go healthy; the first boot seeds the schemas, outbox/idempotency tables and demo users/products.
- Open http://localhost:3000 and sign in with one of the seeded demo accounts from project/pg-init-scripts/sql/20-store-schema.sql.
- Place an order and watch the state move: POST /ms/api/v1/order/product with an Idempotency-Key inserts product_trx as CREATED plus its outbox and idempotency rows in one transaction.
- Create the payment: POST /ms/api/v1/payment/create/SHOPEEPAY?transaction_id= moves CREATED to READY and returns a CheckoutUrl; calling it twice returns the same url.
- A signed SUCCEEDED callback to POST /ms/api/v1/payment/notify (with x-callback-signature and x-callback-timestamp) drives READY to SUCCESS, and within 5s the poller writes INVOICE_DIR/invoice_<tx>.json; a forged signature is rejected with 401.
- Refund with POST /ms/api/v1/payment/refund (SUCCESS to REFUND) - a second refund returns 409 - then read the audit trail at GET /v1/logs?limit=.
[ 05 ] TRADEOFFS
What it does not do
- The README publishes no benchmark numbers: the service-split decision is explicitly deferred until a load measurement justifies it, so there is no latency or throughput evidence to quote.
- Callback verification is wide open when NOTIFY_SECRET is unset - the README documents that as a local-dev-only state, not a deployable one.
- Sessions are short-lived opaque tokens held in memory, so they do not survive a restart or span instances.
- The Java/Spring services are decommissioned and no longer in the tree - only the Go monolith, the partner stub and the Next.js frontend ship, with git history (and one leftover diagram) preserving the old design.
[ 06 ] SOURCE
Read the code
The full implementation, tests and documentation live in the repository.