<-
November 11, 2025

From early‑stage shortcuts to a ledger of record: our journey to reliable money movement

Jinshan Jia

Our money movement challenge

Parafin’s capital products move millions of dollars every day. In the company’s earliest phase, we made the classic tradeoff: optimize for speed. Funding and repayments lived in product tables; a user’s “balance” was recomputed on the fly:

balance = principal + fees – Σ(repayments)

The worked, until reality crept in:

  • State drift. Funding transfers can be returned, reversed, or retried; repayments can bounce or be refunded. We kept adding state‑management branches to cover edge cases, many of which are hard to even enumerate up front, so derived balances occasionally went out of sync.
  • Duplicated logic. Reporting and Accounting re‑implemented the same balance formula (plus growing edge‑case branches) inside analytic pipelines.
  • Feature tax. Every new collection method — manual repayments, third‑party processors, discounts — added another table and another if to “the formula.”

balance = principal + fees – Σ(repayments) – Σ(manual_repayments) – fee_discount – Σ(generic_activity) …

Early‑stage velocity gave us a product, customers, and signal. But it also left us with two sources of “truth” (backend vs. reporting) and rising engineering costs with each new feature. Eventually we hit a wall: the model wasn’t extensible.

Our solution: build a ledger system for every balance‑impacting event

We put a product‑agnostic ledger at the center of money movement. The rule is simple: if an event changes a balance, it must be represented as a ledger transaction first; product state is derived from ledger state, not vice versa.

To make that tractable across teams and vendors, we created layered, vendor‑agnostic services:

  • MoneyMovementService (our integration to Modern Treasury) — integration layers that speak to Modern Treasury’s APIs, webhooks, and idempotency.
  • CashierService — a stable façade exposing “create ledger transaction,” “create payment order,” “reverse,” “return,” and “get balances.”
  • CapitalCashierService — translates product events (funding, repayment, fee waivers/discounts) into generic ledger or money‑movement operations.
  • Product services — keep only product logic: how much to fund, how to compute fees, when to collect.

This separation lets us add vendors or rails without touching product logic and ensures every balance‑impacting event flows through the same contract.

A one‑minute primer on ledgers

sample parafin ledger

A ledger is an append‑only, double‑entry log of monetary events. Each ledger transaction contains at least two ledger entries — one debit, one credit — that net to zero, producing auditable, replayable balances per account.

(If you’re new to double‑entry, Modern Treasury’s docs and journal series are a great grounding in accounts, entries, transactions, and immutability.)

How we implemented the ledger

Build vs. buy: why we chose a vendor

When we decided to build a ledger-first system, we first asked whether to build our own ledger or use a vendor. As a startup, our team is small and we’re deliberate about where we invest engineering effort. We want to focus on Parafin’s core value, not re-solve problems that infrastructure providers already handle well.

Building a ledger in-house would have meant significant work to achieve durability, consistency, and reconciliation guarantees. Instead, we chose to evaluate external ledger vendors that could give us those primitives out of the box. After comparing a few options, we decided on Modern Treasury as the best fit for our needs.

Why Modern Treasury (MT)

  • Existing Footprint. We already used MT for payments, so adopting their ledger meant less integration work.
  • Automatic Status Sync. MT links ledger transactions to underlying payment objects. For instance, it cancels the ledger transaction if a payment fails and posts a reversal if a return arrives. This keeps money movement and ledger in lockstep.
  • APIs + Warehouse. MT provides straightforward APIs and a synced data warehouse, which matters for reporting and reconciliation.

Using MT lets our team focus on product semantics rather than building core ledger infrastructure from scratch.

Translating product events to ledger transactions

The CapitalCashierService converts business events into balanced entries. For example, funding user1 with $1,000 principal and a $10 fee becomes one transaction with four entries:

DEBIT   merchant_outstanding_balance         $1,000   (type=principal)
DEBIT   merchant_outstanding_balance         $10      (type=fee)
CREDIT  parafin_disbursement_account         $1,000   (type=principal)
CREDIT  parafin_fee_revenue_account          $10      (type=fee)

The resulting MT transaction status (e.g., pending → posted) is now the source of truth for product state. Entry metadata (e.g., type=principal|fee) gives Accounting the dimensions they need without re‑deriving context later.

Making the write path bulletproof (idempotent by construction)

Network blips and 5xxs are normal at scale. We designed the write path so they can’t corrupt state:

  • Write‑ahead operation records. Before any call to MT (ledger transaction, payment order, reversal), we create a durable operation row, then update it based on MT’s response. This gives us a replayable intent and an audit trail.
  • Idempotent retries via Temporal. We standardize failure semantics: retryables (timeouts/5xx) set state Unknown with allowRetry=false and are retried by Temporal with the same idempotency key; non‑retryables (4xx) set state Failed with allowRetry=true for a corrected new attempt.
  • Operational reconciliation. Healing and reconciliation jobs compare our operation tables with MT’s warehouse to catch stuck or mismatched operations.

Tradeoff: This adds schema and workflow surface area, but it converts transient failures from scary to routine and prevents duplicate or missing money movement. It’s a tradeoff worth making for correctness.

Making the read path fast and dependable (webhooks + cache)

Calling a third‑party API for every balance read or status check is slow and fragile under rate limits, so we built two mechanisms:

  1. Webhook‑fed local mirror. MT webhooks populate local tables and trigger cache updates so most reads never leave our VPC.
  2. A lock‑versioned balance cache. We cache balances per ledger account alongside ledger_account_lock_version. Writers update the cache on ledger transaction creation/updates; readers hit the cache and refresh asynchronously if stale. We also reconcile cache lock‑versions against MT’s data warehouse on a regular cadence; if versions diverge, we refresh affected accounts asynchronously.
  3. Thanks to the Modern Treasury team for partnering with us to add webhook support that enables this flow.

This kept correctness while cutting external calls by ~80% and improving p95 latency for balance reads.

Tradeoff: Caching introduces eventual consistency risk. The lock‑version discipline and periodic reconciliation are the guardrails that make it safe.

Migrating safely: shadow mode → staged switchover

We launched in shadow mode first. Every money movement produced a ledger transaction in MT, but product UIs and APIs still served balances from the legacy path. We double‑read and logged any discrepancies between the two. Once drift hit ~zero, we gradually flipped traffic — first a slice of businesses, then 100% — and finally removed double‑writes.

Shadow mode immediately paid for itself: we found subtle bugs in the old calculation logic (e.g., rare refund edge cases) that were hard to detect in the legacy system.

Tradeoff: We carried redundant processing and duplicate plumbing briefly, but it bought trust with stakeholders and a clean roll‑out.

Benefits we’re seeing now

By unifying around a single ledger-backed balance, we’ve improved accuracy, reliability, and speed across products and systems, making it easier to launch, reconcile, and operate at scale.

  • One canonical balance. Teams across the company query the same CashierService balance, backed by MT ledger data. No more re‑implementing SQL in dashboards or pipelines; the ledger is the source of truth.
  • Exact reconciliation. We’ve achieved reconciliation to the dollar between our ledger and actual money movement, a milestone that’s notoriously hard for most companies to reach. Every ledger entry corresponds precisely to a real-world transaction, eliminating reconciliation gaps and manual adjustments.
  • Operational clarity. Returns and reversals propagate from MT to ledger status to product state, eliminating drift between product tables and real money movement.
  • Simpler launches. New balance‑impacting events are translations into ledger transactions—no schema contortions or custom balance math.
    • Without a ledger, our recent product launches with Pay Over Time and our Spend Card would have been much more complicated.
  • Performance & reliability. The balance cache handles the hot path while cutting MT calls by ~80%, insulating us from rate limits and transient outages.

What’s next

The ledger is no longer “back office”. It’s a first-class part of our product surface. There’s compelling work ahead.

  • Tighter balance enforcement. We're planning to expand balance locks and lock-version checks on money-out flows so authorization and recording share the same guardrails.
  • Richer statements & as-of history. We'll continue improving historical queries and user-friendly explanations that trace product state directly to ledger entries.

Beyond correctness and auditability, we’re also pushing toward full financial reconciliation.

With our product ledger solidified, the next frontier is extending ledger coverage to the bank and reporting layers, bringing true end-to-end reconciliation.

  • Ledgering every cash account for continuous reconciliation. Today, all money movement events are ledgered through internal omnibus accounts in our CashierService. However, these accounts are not linked to our corporate cash accounts, which means we can’t yet use Modern Treasury’s integrated payments + ledger for automatic reconciliation. Next, we’re expanding our ledger coverage to each of our 25+ corporate accounts to enable real-time, intraday reconciliation against bank balances. This will shorten time-to-detect for anomalies and let us retire much of the accounting team’s reconciliation work.
  • Reporting pipeline refactor. Some of our reporting still relies on legacy, non-ledger sources. As we migrate those to use the ledger, we’re excited to gain the benefits of reporting “for free” as we build new lending products—because reporting is now decoupled from business logic and flows directly from the ledger itself.
  • Faster product enablement. We will be investing in templates and tooling that make new product launches a matter of configuration over code.

If you’re wrestling with drifting balances or exploding schema complexity, our strongest advice is simple: force every balance‑impacting event through a double‑entry ledger and derive product state from that record of truth. The rest — retries, migrations, caching — becomes an engineering discipline rather than a guessing game. For a quick primer (and battle‑tested patterns), Modern Treasury’s “What is a Ledger” and scaling series are excellent resources.

We’re growing the team building this platform. If designing reliable, auditable money movement at scale sounds fun, come work with us.