Why every fintech startup eventually builds its own ledger
FinanceOps started with a third-party ledger. Within 18 months we built our own. Ledger semantics are too coupled to business rules for abstraction.
FinanceOps started with a third-party ledger service. Modern, well-documented, API-first. It handled double-entry bookkeeping, multi-currency transactions, and balance tracking. We integrated it in two weeks. For the first six months, it worked perfectly.
Within eighteen months, we had built our own ledger from scratch. Not because the third-party service was bad. Because ledger semantics are so deeply coupled to business rules that the abstraction boundary was in the wrong place.
By this point I cared less about sounding smart and more about making the tradeoff legible. It also builds on what I learned earlier in “Startup speed and enterprise readiness are not opposites.” The systems had enough history that every database or eventing opinion had receipts behind it. That is the same posture I now bring to longer-lived experiments like ftryos and pipeline-sdk: if the constraint is real, say it plainly and design around it.
Why Third-Party Ledgers Break Down
A general-purpose ledger service makes assumptions about how financial transactions work. These assumptions are reasonable defaults that are wrong for specific businesses. The divergence between the default model and your business model widens with every new feature.
- Transaction timing. Our third-party ledger assumed transactions were atomic: debit one account, credit another, done. Our business requires multi-stage transactions that span hours. A payment comes in, gets held for compliance review, gets partially released, and then fully settled. The ledger had no concept of a transaction in multiple stages.
- Reversal semantics. The third-party ledger modeled reversals as new transactions that offset the original. Our compliance requirements demanded that reversed transactions maintain a link to the original with the reason for reversal, the authorizer, and the timestamp. The ledger’s reversal model was too simple for our audit trail requirements.
- Multi-currency handling. The ledger supported multi-currency, but its exchange rate model assumed a single rate per currency pair per day. Our business handles transactions where the exchange rate is locked at the time of initiation and may differ from the rate at settlement. The ledger could not track both rates on a single transaction.
- Balance calculations. The ledger calculated balances by summing all transactions. Our business requires multiple balance types: available balance, pending balance, held balance, and projected balance. Each type requires different filtering logic. The third-party ledger only supported one balance type.
The Build Decision
Each of these limitations was individually addressable with workarounds. Multi-stage transactions became multiple linked single-stage transactions. Reversal metadata was stored in a separate table. Exchange rates were tracked in application code. Balance types were calculated in the application layer.
The problem was that the workarounds accumulated until the application code was effectively reimplementing the ledger. We were paying for a ledger service while maintaining a parallel accounting system in our application code. The third-party ledger was a data store, not a business logic engine. And the business logic was where all the complexity lived.
The migration took three months. One engineer built the core ledger. Another engineer wrote the data migration. A third verified every balance against the old system. We ran both systems in parallel for four weeks to verify consistency. The new ledger matched the old one to the penny.
What Our Ledger Looks Like
The FinanceOps ledger is a PostgreSQL-native double-entry system with domain-specific extensions:
- Every transaction is a set of entries that must balance. The database constraint enforces this. An imbalanced transaction cannot be committed.
- Transactions have states: initiated, held, released, settled, reversed. State transitions are explicit with timestamps and authorizer records.
- Each entry has a type that determines which balance calculations include it. The available balance excludes held entries. The pending balance includes them.
- Exchange rates are stored per entry, not per transaction, because individual entries in a multi-currency transaction may use different rates.
- The audit trail is immutable. Entries are never updated or deleted. Corrections are modeled as new entries that reference the corrected entry.
The system is roughly 3,000 lines of TypeScript and 500 lines of PostgreSQL DDL. It is not complicated. It is specific. Every line reflects a business rule that our third-party ledger could not express.
The Pattern
Every fintech startup I know has gone through this same progression. Start with a third-party ledger. Hit limitations. Work around them. Accumulate workarounds until the application is a shadow ledger. Build the real ledger. Migrate.
Earlier in this story I was mostly trying to survive the blast radius myself. Here I was trying to design a system where the team did not need heroics in the first place. The same philosophy now shapes ftryos and pipeline-sdk.
Ledger semantics are too deeply coupled to business rules for a generic abstraction to serve well. The abstraction boundary between “ledger” and “business logic” is a fiction. In fintech, the ledger is the business logic.
If I were starting FinanceOps again, I would build the ledger from day one. Not because the third-party service was bad. Because the time spent integrating, working around limitations, and eventually migrating exceeded the time a custom build would have taken. The lesson applies beyond ledgers to any system where the business logic and the data model are the same thing: CRM for complex sales processes, scheduling for healthcare, and inventory for logistics. When the domain is the system, buy-versus-build always resolves to build.
The custom ledger emerged because no off-the-shelf solution handled our specific combination of multi-currency support, real-time reconciliation, and regulatory reporting requirements. We tried three commercial options before accepting that the integration cost exceeded the build cost. The custom ledger took four months to reach production parity with the commercial solution it replaced, but it has required less maintenance in the eighteen months since because it was designed for our exact use case rather than adapted from a general-purpose tool. Build decisions are expensive, but the maintenance cost of fighting a commercial tool that does not fit your domain is often more expensive over a three-year horizon.