The challenge
Money systems rarely fail loudly. They fail in quiet ways: a mobile client retries a request and the customer is charged twice, two transfers lock each other's wallets and deadlock, a cached balance drifts away from the real history, or someone edits a past record to hide a mistake.
Each of those needs a deliberate answer in the design. Hoping the framework handles it is how balances end up wrong in production, and wrong money is the kind of bug customers notice first.
How I approached it
- 01
Every mutation is balanced or it doesn't happen
Credits, debits, transfers and reversals all run inside a database transaction and must produce ledger entries where the sum of debits equals the sum of credits, using 4-decimal bcmath arithmetic instead of floats. If the entries don't balance, the whole transaction rolls back.
- 02
Idempotency keys on every money-moving endpoint
Credit, debit and transfer requests require an
Idempotency-Keyheader. A repeated request with the same key replays the stored response instead of moving money twice, and the same key with a different payload is rejected with a 409 conflict. - 03
Deterministic lock ordering for transfers
A transfer needs to lock two wallets. If two opposite transfers lock them in different orders they can deadlock, so wallets are always locked in a deterministic order. Row-level locking also stops concurrent debits from overspending a balance.
- 04
History is never edited
The ledger is append-only. A reversal is a new compensating transaction that points back to the original, so the audit trail always shows what happened and what corrected it.
- 05
Reconciliation as a safety net
A scheduled reconciliation command compares each wallet's cached balance against the ledger and reports any drift, so a bug can't silently corrupt balances for weeks.
What I built
- Designed the schema: ULID wallets, ledger accounts, immutable transactions and ledger entries, idempotency keys and audit logs.
- Built domain services (Wallet, Transfer, Ledger, Reconciliation) with typed exceptions for each failure mode.
- Added token authentication with Laravel Sanctum, role-based admin endpoints and rate-limited financial mutations.
- Wrote feature tests covering concurrent debits, idempotency, reversals, transfers, reconciliation and auth.
- Documented the architecture and design decisions in
docs/FINCORE_WALKTHROUGH.mdin the repository.
What this project demonstrates
- How to design money flows that stay correct under retries and concurrency
- Writing tests for the failure modes, not just the happy path
- Documenting decisions so another engineer can take the code over