Double-payment race
- Control
- Serializable transaction and row-level lock
- Residual risk
- External payment providers still need their own idempotency contract.
Built a BNPL microservices platform where money remains exact, retries remain safe, and concurrent payments cannot silently double-apply.

Merchant UI selected.
Installment payments combine money invariants, concurrent requests, authentication, search, and merchant-facing workflow in one compact system.
The project focuses on the failure modes that make finance software difficult: rounding drift, duplicate payment, partial state, and credentials living in the wrong client storage.
A full-stack Buy Now, Pay Later (BNPL) microservices platform modeled after services like Sezzle. Three independently deployed Go services backed by a React/TypeScript merchant dashboard, all orchestrated with Docker Compose.
The BNPL Engine handles order creation and installment payment processing. Money is stored as integers (cents) - never floats - to avoid IEEE 754 rounding errors. Payment splitting guarantees the sum always equals the original total: remainder cents are distributed to the earliest installments. SELECT FOR UPDATE row locks prevent double-payment race conditions under concurrent load. The service ships with 24 tests across unit, integration (real Postgres), and full HTTP end-to-end layers.
The Merchant API adds JWT authentication, Elasticsearch-powered transaction search, and Postgres aggregate stats. The React dashboard surfaces these through debounced search, paginated transaction tables, an installment timeline per order, and protected routes - JWT stored in memory, never localStorage, to avoid XSS exposure.
Make exactness a type-level convention.
Serialize the critical state transition.
Separate discoverability from financial truth.
No control is presented as total risk elimination.
01// Money is int64 cents, never float. The installment sum is an invariant.02func (e *Engine) PayInstallment(ctx context.Context, id InstallmentID) error {03 tx, err := e.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})04 if err != nil {05 return err06 }07 defer tx.Rollback()08 09 // Row lock prevents a double-payment race under concurrent load.10 var status string11 err = tx.QueryRowContext(ctx,12 `SELECT status FROM installments WHERE id = $1 FOR UPDATE`,13 id).Scan(&status)14 if err != nil {15 return err16 }17 if status == "paid" {18 return ErrAlreadyPaid // idempotent: retries are safe19 }20 21 if _, err = tx.ExecContext(ctx,22 `UPDATE installments SET status = 'paid', paid_at = now() WHERE id = $1`,23 id); err != nil {24 return err25 }26 return tx.Commit()27}28 29// splitCents distributes a total so the parts ALWAYS sum back to the original.30func splitCents(total int64, n int) []int64 {31 base, rem := total/int64(n), total%int64(n)32 out := make([]int64, n)33 for i := range out {34 out[i] = base35 if int64(i) < rem { // remainder to the earliest installments36 out[i]++37 }38 }39 return out // sum(out) == total, guaranteed40}Serializable tx + row lock kills double-payment; integer cents keep the split exact.
One transaction holds the row lock; the other observes the resulting paid state.
Payment truth remains in PostgreSQL while merchant discovery is degraded.
Payment state remains safe under concurrent requests
Installment totals remain exact without floating-point arithmetic
Merchant search and dashboards remain separate from the authoritative ledger