Engineering Decisions
Why This Is Harder Than It Looks
The Whole Idea in One Line
The Three Tables
Sql
Idempotency, Concretely
Sql
Why Per-Page Charging Beats Per-Search
Refunds Are Forward, Not Backward
ledger 8420: -3 credits, action="unlock.email", target=cand_91, status="committed"
ledger 8421: +3 credits, action="unlock.email", target=cand_91, status="rolled_back",
refunds_id=8420
Two reasons this matters.
- Audit trail tells the truth. The user was charged, then refunded, and both events happened. Editing or deleting the original entry silently breaks any downstream report or invoice that already read it.
- Cache row comes out too. Otherwise the next attempt at the same action will short-circuit on the idempotency check and never re-charge. The rollback is two writes - a positive ledger entry and a delete from credit_cache - inside one transaction.
Pre-Flight Checks Are Not Optional
Ts
What Goes In the Ledger
- account_id
- amount - signed; charges are negative, grants and refunds are positive
- action - search.reveal, unlock.email, plan.grant, admin.adjust
- target_id - nullable for grants and adjustments
- status - committed, rolled_back, pending
- created_at
- metadata jsonb
What goes in `metadata`?
Why no `is_admin` column?
Why immutable status enums?
What I Would Tell Past Me
- Put the cache insert inside the same transaction as the ledger write. If they can drift, they will.
- Refund forward. Always. Do not edit. Do not delete. The audit log is what you will wish you had when the support email comes in.
- Check the balance before you start the work, not after. The number of failure modes drops by half.
- Keep action strings flat. Hierarchies tempt you, then they fight you. unlock.email and unlock.phone is fine; nested category tables is a regret.
- Reconcile in CI. A nightly job that walks the ledger and confirms account.balance matches catches a class of bug once and forever.