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.