Skip to content
  • Pricing
Sign inContact sales
Blog

Category

Engineering

Written by

Avy Faingezicht
Avy Faingezicht
Engineering Lead
Copied link
Blog
Engineering

Version Control for Financial Reality

The data models we built to handle accounting for 25k+ investment vehicles

Apr 1, 2026 — 11 min read

Written by

Avy Faingezicht
Avy Faingezicht
Engineering Lead
Copied link

New information changes our understanding of the past. In fund accounting, you can't just update a record and move on. The system has to preserve provenance, make corrections explicit, and be able to explain what's recorded in the ledger: "This is what we knew yesterday, so we booked X, which explains why the report we ran says Y even though today it says Z, since we also booked W." Getting that right, reliably, across 25k+ investment vehicles, is the core engineering problem discussed in this post.

In a previous blog post, I explained that part of our team's mandate is programmatically turning the legal and economic reality of our customers' funds into deterministic accounting. This post goes a level deeper, not the what but the how, by defining the data models we built to enable it.

When things happened vs. when we learned about them

Venture funds, and the assets they hold, evolve over time. We learn about these updates as we receive documents. Sometimes those are simple notes in an email or a signed PDF. Other times, they can be complex waterfall models and proformas in Excel spreadsheets. We ingest these actions through a mix of internal and external user input in our UI, with both human and AI operations agents triggering chains of updates. These real-world events are piped through and translated into general ledger (GL) entries, which aggregate into financial metrics.

Roughly, the motion looks like this:

real-world event → document processed → application db → accounting transaction → ledger entries → financial statements

Think of it as version control for financial reality. Bitemporal filtering markers allow us to distinguish between effective date and knowledge date:

  • The effective date is when the event happened.
  • The knowledge date is when we learned of and recorded it.

Every transaction and general ledger entry has these two dates associated with it. That distinction is one of the attributes that turns a CRUD application into an accounting system, enabling rigorous review and reporting workflows.

The core abstractions

On top of this time construct, the core abstraction that allows us to track provenance is split between two concepts, which allow us to reproduce outputs given known inputs:

  • Command: what changed in the world?
  • AccountingTransaction: given that change, what ledger entries should we book?

A Command operates close to user intent. Our users do not ask our system to debit one account and credit another. They buy shares in a company. Or later, they learn those shares should be marked up because the company raised a new round. The Command updates the fund state in our database, and creates the relevant transactions once it's applied. The AccountingTransaction stores the event data needed to book the accounting and generates the corresponding ledger entries. For each workflow supported by our system, we worked closely with a dedicated team of CPAs to understand the myriad edge cases and break them into atomic units.

Commands

A Command is our API for changing fund state.

That state is broader than any one model or subsystem. It includes the legal and economic facts we store about a fund, including commitments, fees, assets, and cash obligations. A command is the thing that says: this specific operation is happening, under these rules, with this effective date, initiated by this user or system, with this evidence.

This model is useful because:

  1. It constrains how state can change. We do not want ten ways to represent “valuation changed” scattered across console scripts, admin actions, callbacks, and background jobs.
  2. It gives us provenance. If an accountant is staring at a ledger line three months later, we want to be able to trace an entry back to the accounting event, then to the command, then to the user or system action that caused it.
  3. It gives us a workflow object that can act as the core of a maker/checker mechanism. Commands can be created, approved, rejected, and reviewed. That matters because some changes are safe to apply automatically and some are not, especially when they are applied retroactively. Approvals are part of the state machine.

A command updates state in our database. When applied, it creates the necessary AccountingTransaction records as part of the same atomic operation. It does not touch the ledger directly. That separation is deliberate.

Accounting Transactions

An AccountingTransaction represents an accounting event, downstream of a general state mutation.

The job of an accounting transaction is to hold the event data needed to do the accounting for that event. For each type of transaction, a handler generates metadata and creates ledger entries according to the rules for that event type. This is an important design rule: ledger entries are generated only from the transaction’s event_data and existing ledger entries, never by reaching back into broader fund state.

Why be this strict?

Because otherwise, the booking logic becomes nondeterministic. If a transaction handler can time travel and peek at whatever the current fund models say today, then rerunning the same accounting event tomorrow can produce different entries than it did yesterday. That is how you accidentally rewrite history.

The accounting transaction stores inputs that define a change. We record the relevant amounts, dates, and identifiers as understood when the event was created. This boundary is enforced in code.

A simple example: Buy an asset

Suppose a venture fund wires $100k to buy SomeCo shares on October 1st.

At the business-logic layer, that is a Command:

command = BuyAssetCommand.create!(
fund: fund,
company: someco,
effective_date: "2025-10-01",
shares: 10_000,
purchase_price_usd: 100_000
)

When applied, the Command updates the fund state: in the application database the fund now owns the asset. It also creates an accounting transaction representing the purchase:

AssetPurchasedAccountingTransaction.create!(
event_data: {
effective_date: "2025-10-01",
knowledge_date: "2025-10-02",
asset_id: someco_asset.id,
amount_usd: 100_000
}
)

That transaction books a simple entry:

effective_date: "2025-10-01"
knowledge_date: "2025-10-02"
debit SomeCo Investment (Cost) 100_000
credit Cash 100_000

The important thing is that the accounting transaction stores the change, not a pointer to the current state. The command is allowed to look at the fund state and decide what happened. The accounting transaction is not.

Now suppose SomeCo raises a new round, on 12/15, implying our position is worth $150k. But we do not learn about that immediately. The round happens on 12/15, the books for 12/31 get closed on 1/15, and we only learn about the new round on 1/30. That new knowledge should come in as a new command:

command = MarkupAssetCommand.create!(
fund: fund,
asset: someco_asset,
effective_date: "2025-12-15",
fair_value_usd: 150_000
)

The business logic compares that to the existing balances, and turns the $150k into a new accounting transaction for the $50k change:

AssetMarkedAccountingTransaction.create!(
event_data: {
effective_date: "2025-12-15",
knowledge_date: "2026-01-30",
asset_id: someco_asset.id,
change_in_value_usd: 50_000
}
)

That transaction books an unrealized gains entry:

effective_date: "2025-12-15"
knowledge_date: "2026-01-30"
debit Investment unrealized gains 50_000
credit Net change in unrealized gains on investments 50_000

And with this entry in place, even though the books for 12/31 are closed, without rewriting the past we've established why the value on our balance sheet was $100k when we produced the report, even though new data tells us the real value should have been $150k.

A more complex example: Capital calls in practice

When funds require cash to make investments, they call capital from their limited partners. The capital call flow is a good example of why we split Command and AccountingTransaction instead of writing directly to the ledger from workflow code.

In this flow, there are at least three distinct moments:

  1. We initiate the call (operational state changes)
  2. The call becomes due (obligation is recognized)
  3. Cash is received (obligation is settled)

In our code, those map to separate commands and transactions:

# simplified, illustrative flow
CPTR::Command::CapitalCalls::InitiateCapitalCall.create!(...)
CPTR::Command::CapitalCalls::RecognizeCapitalCallDue.create!(...)
CPTR::Command::RecordMemberCommitmentFunding.create!(...)

Initiate capital call: state, not booking.

InitiateCapitalCall creates a CapitalCall record, tying the percentage called, the relevant dates and the fund's identifier. This captures legal/economic intent, but does not book to the ledger. “We called capital” is not yet the same thing as “it is due now.”

Recognize due: create accounting obligation

RecognizeCapitalCallDue computes the cumulative percent due as of the effective date, calculates each impacted member’s due amount based on their commitment, and creates the relevant AccountingTransaction.

From there, the transaction handler generates metadata and ledger entries. In the common case, think of it as:

debit Outstanding Called Contributions 100_000
credit Capital Contributions 100_000

The production logic is more nuanced than this textbook line. It true-ups against balances already on the books, including advanced and in-kind contribution buckets, so we can easily see who owes what.

3) Record funding: settle the receivable with cash

When cash arrives, RecordMemberCommitmentFunding creates its own accounting transactions, which books for every member:

debit Capital Collection Cash 100_000
credit Outstanding Capital Contributions 100_000

If the member had overfunded, after outstanding is relieved the remaining amounts are credited to Advanced Contributions rather than distorting contribution balances. If needed, late-admission penalties are also handled in this same deterministic sequence.

Each accounting transaction carries the event payload needed for booking, then derives entries from that payload plus ledger history.

The pattern generalizes

Buying an asset is a simple example, but this shape allows us to model future markups, liquidations, capital calls, and money movements. They all follow the same shape:

  • A command models the real-world operation
  • An accounting transaction models the accounting event
  • The ledger entries fall out deterministically from stored event data

Ultimately, what this setup enables us to do is to separate concerns into very small actions with clearly defined business logic, which we can book against the accounts that our fund accountants specified. The AccountingTransaction handlers understand which balances should be consumed if anything needs to be trued up with explicit after-the-fact corrections, which accounts are to be credited or debited, and can operate independently of the rest of the system state.

Growing into our current system presented us with a lot of challenges, but we've proven that it works. The team now has a core platform on which we can grow incrementally, taking on more complex fund structures, and continuing to deal with the creative legal constructs our customers demand.


Latest articles

Engineering

Everyone Is a Data Analyst Now

Jun 10, 2026 — 15 min read
Data

Is Venture Capital Intrinsically Cyclical?

Jun 8, 2026 — 16 min read
Engineering

The Interview That Ships to Production

May 15, 2026 — 7 min read
;
Contact salesSign in

Products

Fund Administration

  • Venture Funds
  • Rolling Funds
  • Scout Funds
  • SPVs
  • Roll Up Vehicles

Investor Management

  • Digital Subscriptions
  • Data Room

Pricing + Returns

  • Pricing
  • VC Fund Performance Calculator
  • RUV Calculator

Resources

Learn

  • Blog
  • Help Center
  • Education Center
  • Data Center

Company

  • About Us
  • Careers
  • Engineering

By AngelList

  • Rollups
  • Meridian
TermsPrivacyDisclosures© AL Advisors Management Inc.
Disclaimer:

The information contained herein is provided for informational and discussion purposes only and is not intended to be a recommendation for any investment, service, product, or other advice of any kind, and shall not constitute or imply an offer of any kind. Any investment opportunities and/or products or services shown here will only be completed pursuant to formal offering materials, a letter of intent, and/or any other agreements as determined by AngelList containing full details regarding risks, minimum investment, fees, and expenses of such transaction. The terms of any product, service, or particular investment opportunity, including size, costs, and other characteristics, are set forth in the applicable constituent documents for such product, service or particular investment opportunity and may differ materially from those presented in this presentation. Such terms are subject to change without notice. For more information on AngelList and its products and services, please see here.

Quotes included in these materials related to AngelList's services should not be construed in any way as an endorsement of AngelList's advice, analysis, or other service rendered to its clients.