
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.
// Money is int64 cents, never float. The installment sum is an invariant.
func (e *Engine) PayInstallment(ctx context.Context, id InstallmentID) error {
tx, err := e.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
if err != nil {
return err
}
defer tx.Rollback()
// Row lock prevents a double-payment race under concurrent load.
var status string
err = tx.QueryRowContext(ctx,
`SELECT status FROM installments WHERE id = $1 FOR UPDATE`,
id).Scan(&status)
if err != nil {
return err
}
if status == "paid" {
return ErrAlreadyPaid // idempotent: retries are safe
}
if _, err = tx.ExecContext(ctx,
`UPDATE installments SET status = 'paid', paid_at = now() WHERE id = $1`,
id); err != nil {
return err
}
return tx.Commit()
}
// splitCents distributes a total so the parts ALWAYS sum back to the original.
func splitCents(total int64, n int) []int64 {
base, rem := total/int64(n), total%int64(n)
out := make([]int64, n)
for i := range out {
out[i] = base
if int64(i) < rem { // remainder to the earliest installments
out[i]++
}
}
return out // sum(out) == total, guaranteed
}Serializable tx + row lock kills double-payment; integer cents keep the split exact.