Event Sourcing & Replay¶
The apps.operations module provides an event-sourced replay engine for loan reconstruction. This complements the pghistory audit trail by providing a domain-specific, financial-event-focused mechanism for reconstructing loan state at any point in time.
Overview¶
Traditional loan systems store only current state --- if a loan's balance is $10,000 today, there's no built-in way to determine what it was on any arbitrary past date without complex queries across multiple tables. The event sourcing model solves this by recording every financial event as an immutable log entry.
LoanEvent Model¶
The LoanEvent model stores timestamped, immutable events:
| Field | Type | Description |
|---|---|---|
loan |
FK | The loan this event belongs to |
event_type |
TextChoices | Type of financial event |
event_data |
JSONB | Event-specific payload |
effective_date |
Date | When this event takes effect |
sequence_number |
Integer | Ordering within a loan's event stream |
Event Types¶
| Event Type | Trigger | Key Data |
|---|---|---|
origination |
Loan created | Principal, rate, term |
disbursement |
Loan funded | Amount, method |
payment |
Payment applied | Amount, allocation breakdown |
interest_accrual |
Daily accrual | Amount, rate, day count |
fee_assessment |
Fee assessed | Amount, fee type |
fee_waiver |
Fee waived | Amount, waived by |
modification |
Terms changed | Before/after snapshots |
forbearance_start |
Forbearance begins | Type, reduced payment |
forbearance_end |
Forbearance ends | Post-forbearance action |
deferment |
Periods deferred | Count, interest treatment |
charge_off |
Loan charged off | Write-off amount |
reversal |
Payment reversed | Original payment ref |
Replay Engine¶
The replay_loan() function reconstructs loan state at any point in time:
from apps.operations.services import replay_loan
# What was this loan's state on January 15th?
loan_state = await replay_loan(loan_id=loan.id, as_of_date=date(2026, 1, 15))
How Replay Works¶
- Start with an empty loan state
- Fetch all
LoanEventrecords for the loan, ordered bysequence_number - Filter to events with
effective_date <= as_of_date - For each event, apply the corresponding handler to mutate the state
- Return the reconstructed state
Each event type has a dedicated handler function that knows how to apply that event's effects to the running state (e.g., an interest_accrual event increases the interest receivable balance).
Use Cases¶
Point-in-Time Balance Reconstruction¶
Determine exact balances at any historical date --- useful for customer inquiries, audits, and regulatory reporting:
- "What was the outstanding balance on March 1st?"
- "How much interest had accrued by the end of Q1?"
Audit and Compliance¶
Provide a complete, verifiable history of every financial event that affected a loan. Each event is immutable and sequenced, creating an unbroken chain of custody.
Regulatory Reporting¶
Reconstruct loan portfolios as of specific reporting dates. Useful for generating accurate historical snapshots for regulatory filings.
Dispute Resolution¶
When a borrower disputes a balance or payment application, replay the loan to the disputed date to show the exact sequence of events.
Relationship to pghistory¶
The event sourcing model is complementary to pghistory, not duplicative:
| Aspect | pghistory | LoanEvent |
|---|---|---|
| Scope | All tracked models (row-level snapshots) | Financial events only |
| Granularity | Every field change | Business-level events |
| Purpose | Audit trail ("who changed what") | State reconstruction ("what was the balance on date X") |
| Trigger | PostgreSQL triggers (automatic) | Service layer (explicit) |
| Query pattern | "Show me all changes to this loan" | "Reconstruct this loan's state as of date X" |
pghistory answers "what changed and when." Event sourcing answers "what was the state at any point in time, and why."
See Also¶
- Architecture Overview --- Where event sourcing fits in the system
- Cross-Module Communication --- How events are emitted from services