Practice · Payments · Card 6
Wallet balance: balance column or ledger?
Users hold store credit. Many small credits and debits. Two reasonable shapes. The right one depends on what you'll need to ask.
The choice
Two candidate schemas, both valid Rails.
# Option 1: balance column
class User < ApplicationRecord
# users.wallet_balance_cents
def credit!(amount, source:)
increment!(:wallet_balance_cents, amount)
end
def debit!(amount, source:)
decrement!(:wallet_balance_cents, amount)
end
end
# Option 2: ledger
class LedgerEntry < ApplicationRecord
# ledger_entries.user_id, .amount_cents, .kind, .source_type, .source_id
end
class User < ApplicationRecord
has_many :ledger_entries
def wallet_balance_cents
ledger_entries.sum(:amount_cents)
end
end The question
When does the balance-column shape break, and when is the ledger the obvious choice?
Take a moment. The balance column is simpler. The ledger is more correct. What does "more correct" buy you, and when does the cost of the SUM matter?
When to pick which
Use the ledger when:
- You need an audit trail — "how did this user end up with $42.13?" — the row history is your answer.
- Balances accumulate over many events, especially with reversals (refunds, adjustments) that need to reference the original entry.
- Concurrent writes are likely. Two concurrent INSERTs don't race the way two concurrent UPDATEs to the same row do.
The balance column is fine when:
- The question is just "did the user pay yes/no" — a flag, not a balance.
- You're not handling money (e.g., a like-count on a post — eventual consistency is acceptable).
- The audit trail is held elsewhere (provider records, event log) and the column is a read-side projection.
For wallets, store credit, escrow accounts, refund pools — the ledger is the right answer. The cost (a SUM on read) almost never matters until your account holds millions of entries, and then you add periodic snapshots.
Wrong takes worth noticing
- "The balance column is always wrong; never use it." Too absolute. For flag-style "did this happen yes/no" cases, a column is fine.
- "The ledger is always more correct; always use it." It costs read performance and complexity. Use it when the audit-trail and concurrency-safety wins justify the cost.
- "Both options have the same concurrency properties." They don't.
User#increment!issues an atomic SQL UPDATE, which is correct at the SQL level but still serializes writers on one row. Ledger INSERTs go to different rows and run concurrently without contention.
The principle
The ledger pattern's value is in correctness, not performance. When SUM gets slow on huge accounts, add a periodic snapshot row and sum forward from there — you almost never need this until you measure it.
Theory
Full walkthrough at Payments · The Ledger Pattern.