Skip to content

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

  1. Start with an empty loan state
  2. Fetch all LoanEvent records for the loan, ordered by sequence_number
  3. Filter to events with effective_date <= as_of_date
  4. For each event, apply the corresponding handler to mutate the state
  5. 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