Double-Entry Accounting Primer
Every financial event produces at least two journal lines: one debit and one credit. The sum of all debits must equal the sum of all credits for every entry, preserving the accounting equation: Assets = Liabilities + Equity.
Core Schema
Account Table
Account (
id BIGINT PRIMARY KEY,
name VARCHAR(128),
type ENUM('asset','liability','equity','revenue','expense'),
currency CHAR(3),
created_at TIMESTAMP
)
JournalEntry Table
JournalEntry (
id BIGINT PRIMARY KEY,
description TEXT,
idempotency_key VARCHAR(128) UNIQUE,
created_at TIMESTAMP
)
JournalLine Table
JournalLine (
id BIGINT PRIMARY KEY,
entry_id BIGINT REFERENCES JournalEntry(id),
account_id BIGINT REFERENCES Account(id),
amount_cents BIGINT,
direction ENUM('debit','credit'),
currency CHAR(3)
)
Balance Rules
- Debits increase asset and expense accounts.
- Credits increase liability, equity, and revenue accounts.
-- Balance for an asset account
SELECT
SUM(CASE WHEN direction = 'debit' THEN amount_cents ELSE 0 END) -
SUM(CASE WHEN direction = 'credit' THEN amount_cents ELSE 0 END) AS balance_cents
FROM JournalLine
WHERE account_id = ?;
Immutability and Corrections
Journal entries and lines are never updated or deleted. This preserves a complete, tamper-evident audit trail. Corrections are made via reversing entries: a new JournalEntry with the same lines but swapped directions, followed by a corrected entry.
Idempotency
The idempotency_key unique constraint on JournalEntry prevents duplicate postings. Callers generate a deterministic key (e.g., payment_{payment_id}_capture) and the database rejects re-insertion silently via INSERT IGNORE or ON CONFLICT DO NOTHING.
Multi-Currency Support
Each JournalLine stores the amount in its original currency. A parallel column stores the USD equivalent at the time of posting, sourced from an exchange rate service. Reporting queries aggregate in USD using the stored rate rather than re-converting.
Running Balance View
SELECT
jl.account_id,
je.created_at,
jl.amount_cents,
jl.direction,
SUM(
CASE WHEN jl.direction = 'debit' THEN jl.amount_cents ELSE -jl.amount_cents END
) OVER (
PARTITION BY jl.account_id
ORDER BY je.created_at
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_balance_cents
FROM JournalLine jl
JOIN JournalEntry je ON jl.entry_id = je.id;
Period Closing
At period end, closing entries zero out revenue and expense accounts by transferring their net balance into retained earnings (an equity account). This resets P&L accounts for the next period while keeping cumulative history in the journal.
Audit Trail
Because JournalEntry is append-only and every mutation is a new entry, the full history of any account is reconstructable by replaying lines in created_at order. No separate audit log table is required.
API Design
POST /entries
{
'idempotency_key': 'payment_42_capture',
'description': 'Capture payment 42',
'lines': [
{'account_id': 101, 'direction': 'debit', 'amount_cents': 5000, 'currency': 'USD'},
{'account_id': 202, 'direction': 'credit', 'amount_cents': 5000, 'currency': 'USD'}
]
}
GET /accounts/{id}/balance
GET /accounts/{id}/history?from=&to=
{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “What is double-entry accounting in a ledger service?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Double-entry accounting means every financial event produces at least two journal lines: one debit and one credit. The sum of all debits must equal the sum of all credits for every entry, preserving the fundamental accounting equation: Assets = Liabilities + Equity. This constraint prevents money from being created or destroyed within the system.”
}
},
{
“@type”: “Question”,
“name”: “Why are ledger journal entries immutable?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Immutability ensures a tamper-evident audit trail required for financial compliance. Journal entries and lines are never updated or deleted. Corrections are made via reversing entries: a new journal entry with the same lines but swapped debit and credit directions, followed by a correctly-stated entry. This preserves the full history of every account balance.”
}
},
{
“@type”: “Question”,
“name”: “How do you prevent duplicate ledger entries?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Each JournalEntry has a unique idempotency_key column. Callers generate a deterministic key tied to the business event, for example 'payment_42_capture'. The database unique constraint rejects re-insertion of the same key, preventing duplicate postings even when the caller retries due to a network failure or timeout.”
}
},
{
“@type”: “Question”,
“name”: “How does a ledger service handle multiple currencies?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Each JournalLine stores the amount in its original currency alongside a USD equivalent computed at the time of posting using an exchange rate service. Reporting queries aggregate in USD using the stored rate rather than re-converting at query time, ensuring historical reports remain stable as exchange rates fluctuate.”
}
}
]
}
See also: Stripe Interview Guide 2026: Process, Bug Bash Round, and Payment Systems
See also: Coinbase Interview Guide
See also: Scale AI Interview Guide 2026: Data Infrastructure, RLHF Pipelines, and ML Engineering