VenduSys
Home/Blog·Architecture

The shape of a ledger:
four entities is enough.

We spent the first six months of VenduSys modeling everything. By month four we had seventeen entities, eight join tables, three "context" objects that no one could quite define, and a permission model that took twenty minutes to explain. Classic.

Then we threw it away and started from a question: what is the smallest set of entities that can model every commerce shape we care about?

The answer turned out to be four.

Party

A person, organization or agent. Buyers, sellers, operators, partners — all projections of the same record. The difference between "buyer" and "seller" is not a type, it is a role on a transaction. A buyer in one move is a seller in the next.

Once you accept that, identity becomes a graph problem rather than a typing problem. KYB, KYC, roles, audit, payout setup — all hang off the same node.

Thing

Anything offered or owned. Products, licenses, services, subscriptions, assets, time. Variants and bundles are projections on top — a SKU is a Thing in a particular shape, a subscription plan is a Thing with a time dimension, a service-hour is a Thing with a capacity dimension.

Most commerce stacks model these as separate entities (Product, Plan, Service, Asset) and then spend years patching the gaps. Treating them as one shape with attributes — perpetual or time-bounded, physical or digital, transferable or not — collapses a surprising amount of complexity.

Move

An event that shifts value or rights between parties. Orders, returns, renewals, transfers, refunds, disputes — all of these are moves. A move has a payer, a payee, a thing (or set of things), an amount, and a state.

Modeling "order" and "refund" as the same kind of object — with different signs — eliminates an entire category of bugs.

Entry

A line in the double-entry ledger. Every Move produces Entries; reports and balances derive deterministically from them. This is the single most important entity in the system, and the one most commerce platforms hide.

If your platform is a system of record for money but your ledger is an afterthought, your platform is a system of plausible deniability.

Why this works

The trick is not the four entities. The trick is what they let you not have:

  • No separate "marketplace order" and "direct order" types
  • No separate "subscription" and "one-time purchase" — both are sequences of moves
  • No separate "wallet" — a balance is a sum of entries against an account
  • No separate "refund engine" — a refund is a reversing move with reversing entries
  • No separate "partner ledger" — partner payouts are just entries against partner accounts

The shape in code

Roughly, in TypeScript:

type Party = { id; kind: 'person' | 'org' | 'machine'; profile; trust };
type Thing = { id; kind; attrs; rights?; meter? };
type Move  = { id; kind; from: PartyId; to: PartyId; thing: ThingId[];
               at; state; reverses?: MoveId };
type Entry = { id; account; move: MoveId;
               side: 'debit' | 'credit'; amount; ccy };

Everything else — orders, invoices, subscriptions, partner programs, marketplaces — is a workflow that composes these. The workflow library is a different post.

What we gave up

Some legibility, honestly. "Order" and "Subscription" are useful nouns in conversations with non-engineers. We keep them as projections in the API surface — but underneath, they are sequences of moves. That gap is the cost.

In exchange, we got a system that doesn't break when a new shape arrives. We will know we got the model right if we never have to add a fifth entity. Ask us again in three years.


T. Roux is a co-founder & CTO of VenduSys. Previously: ledger systems at a fintech, infrastructure at two commerce platforms. Based in Lyon.