← ALL THINKING
BUILD NOTESJUL 2026

Designing an entitlements engine that fits any business

This problem has been following me around for years. At SafetyCulture, I wanted electricians to be fitted out with PPE the moment they came on board, and what each person could have was not a style preference: it was written into their EBA, item by item. More recently I've watched Cargo Crew deal with the nuance of enterprise uniform programs up close: who gets what, how often it renews, whose budget it draws on, and what happens when someone entitled to two shirts orders a third. And in nearly every B2B discovery workshop we run, the same requirement surfaces inside ten minutes, wearing different clothes: give the right people access to the right things, in the right quantity, on someone else's budget.

The words change, the problem doesn't. And when the same problem keeps arriving in different clothes, rebuilding a bespoke answer for every client is malpractice. So we set out to design it once, properly: a strong pattern and a reusable set of APIs for entitlements and credit that we could deploy into any client's platform, whatever they sell and whoever they issue value to. On the surface it's a simple promise: issue someone an allowance, let them spend it, enforce the rules automatically. Underneath, it turned out to be one of the most interesting design problems we've worked on, because almost every rule you'd assume is obvious turns out to be a business decision someone has to make deliberately.

This piece walks through those decisions. Not the code, the thinking.

The shift that creates the problem

Corporate uniform programs used to be a yearly event. Someone in head office collected sizes on a spreadsheet, placed one giant order, and boxes arrived at each site. The program's controls lived in that spreadsheet and in the person who owned it.

That model is dying, and for good reason. Companies want always-on, employee-led ordering: a new hire gets kitted out the week they start, replacements happen when things wear out, and nobody in head office touches a size chart again. But the moment you let every employee shop for themselves, three questions that the spreadsheet used to answer quietly become hard system requirements:

Who pays for it? How much is each person allowed? And what exactly are they allowed to spend it on?

An entitlement is our answer to all three at once. It's a wallet balance issued by an employer to one of its staff, in dollars or points, optionally restricted to specific products or categories, and drawn down automatically when the employee orders. The employer funds the program and sets the rules. The employee just shops. The platform sits in the middle and enforces everything.

Wallet, not voucher

The first design decision sounds small and isn't: an employee can hold several entitlements at once, and together they form a wallet.

Think about how value actually arrives in these programs. A new technician gets a one-off starter pack the day HR creates their account. Then an annual allowance lands every July. Then a project allowance appears for a six-month site deployment. These are different pots of money, with different expiry dates, different rules, sometimes different funding lines inside the employer.

A voucher model (one code, one balance, one use) collapses all of that nuance. A wallet model keeps each pot separate, each with its own lifecycle, and asks the platform to answer a new question: when a cart could be paid from more than one pot, which pays first?

Our rule is: spend the soonest-expiring credit first. It's deterministic, it never asks the employee to choose, and it quietly protects people from the most common failure in allowance programs, which is stranding value in a credit that lapses while a newer one gets used. Nobody reads a help article about this. It just behaves the way a considerate human would.

Ledger, not a number

Here's the decision everything else stands on. An entitlement's balance is not a number we store and update. It's a number we derive from an append-only transaction log that lives inside the entitlement itself.

Six transaction types cover the whole lifecycle: credit (issuance and top-ups), debit (captured against an order), refund (returns and short-shipments flowing back), void (an administrative reversal), adjustment (a manual correction, with a mandatory note), and expire (the write-off when a balance lapses). Every entry is immutable and records the amount, the balance after it, the timestamp, the actor, and a reference, usually the order that caused it.

One entitlement, one ledger: a $500 allowance across its life, with the balance derived from the log.

Why be this disciplined about it? Because the alternative fails slowly and embarrassingly. If the balance is just a stored number, then every bug, race condition or hand edit silently corrupts it, and six months later finance asks why a wallet shows $38.50 and nobody can say. With a ledger, the stored balance is only a convenience, a materialised view. If it ever disagrees with the log, the log wins, and an admin can rebuild any balance from its history and see the drift reported.

The ledger also gives you no-double-spend almost for free. Every transaction's ID is deterministic: an order debit's ID is derived from the order and the entitlement, so capturing the same order twice yields exactly one debit, no matter how many times a nervous checkout retries. The database itself enforces the rule.

The accounting question nobody asks until go-live

At some point in every program like this, someone from finance asks the question that should have been asked in week one: when we issue an employee $500 of allowance, what have we actually created?

The honest answer is: it depends on the commercial model, and the platform's job is to make either answer auditable. If the employer prepays the program, outstanding entitlement balances are effectively a liability, value promised but not yet delivered, and finance needs to see it, age it, and watch it expire. If the operator bills on redemption, the issued balance is more like a commitment than a booked liability, but the same questions still arrive at audit time: what was issued, under whose authority, what was spent, what was corrected and why, and what was written off.

This is where the ledger stops being an engineering nicety and becomes the finance feature. Because every movement of value is a typed, referenced, actor-stamped transaction, every one of those questions is a query, not an investigation. The issuance register is budget in. Redemption joined to orders is budget out. Outstanding balance by organisation is the headline number on the finance dashboard. Manual adjustments and write-offs get their own report, because corrections are exactly where auditors look first, which is why an adjustment can't be saved without a note and only a platform-level admin can make one.

Expiry matters more than people expect, too. A lapsed allowance that just sits there is phantom liability, value that looks owed but never will be spent. So an hourly sweeper closes out past-due entitlements and writes an explicit expire transaction that zeroes the remainder. Nothing fades away informally; everything ends with an entry.

And one boring rule with outsized consequences: there is no floating-point money anywhere in the system. Currency is stored in cents, points in whole integers. Every accountant reading this just nodded.

Rules a human can read

Restrictions are where uniform programs, PPE programs and tool budgets genuinely differ, so this is where the engine earns its "fits any business" claim.

Every entitlement can carry a restriction, a rule deciding what it may pay for: "workwear only", "shirts and pants but not these two SKUs", "winter 2026 range over $50". On top of that sit quotas, which cap quantities per bucket: "max 2 tops, max 1 outerwear". A hospitality allowance and a mine-site PPE program are the same engine with different rules; ANSI-rated gloves and hi-vis for one, aprons and oxford shirts for the other.

Two decisions here matter more than the rule syntax ever will.

First, admins never see an expression language. Rules are built visually and rendered back in plain English everywhere they appear, on the entitlement card, in the admin drawer, and on the employee's "what you can spend it on" page. If a rule can't be read aloud to the HR manager who owns the program, it isn't done.

Second, rules are versioned, and every entitlement stamps the exact version it was issued under. This is the difference between policy and promise. When the policy changes next quarter, the credits already in people's wallets keep behaving exactly as promised on the day they were issued; propagating a new version to existing entitlements is an explicit, deliberate act. A rule edit can never silently rewrite what an employee was told they had.

Checkout that doesn't feel like a payment

All of this machinery exists so that the employee experience can be almost nothing. Staff see a wallet, not a price tag. Product pages show a small badge naming the credit that will cover the item. The cart carries the same signal per line. And at checkout, the engine previews the cart against every active entitlement and applies the best plan automatically: restrictions first, quotas second, soonest-expiring credit third.

Checkout decided by policy: three questions — restrictions, quotas, spend order — between a cart and a payment.

If the wallet covers everything, the card form disappears entirely and the button says "Place Order — fully covered". If it covers part, the remainder routes to the employee's own card in the same flow: "Place Order — $70.00 on card". There is no payment picker, no code to type, nothing to get wrong. Coverage is whole-unit on purpose: a garment is either covered or it isn't, because "we paid 60% of your jacket" is an experience nobody can explain at a barbecue, let alone reconcile.

The under-appreciated part is what happens when things go backwards. A short-shipped shirt, a return, a cancelled order: each writes a refund transaction straight back onto the entitlement that paid, restoring spendable balance without a support ticket. Closing the loop on the unhappy paths is what makes a program trusted enough that a company hands it to five thousand staff.

API-first, and why that pays off later

Everything above is exposed as APIs, and the entitlement data itself lives in commercetools as custom objects, versioned JSON documents sitting inside the same commerce platform as the products, carts and orders. No side database, no separate infrastructure to host, back up or secure. The entitlement carries its own state, its own resolved rules, and its own ledger; commercetools' object versioning gives us safe concurrent writes when two processes touch the same wallet at once.

Storing entitlements natively in the commerce platform has a quietly powerful consequence: an order and the ledger entries that paid for it point at each other. The drawdown is recorded as a real payment object on the order, carrying the full plan of which entitlement covered which line, and the IDs of the ledger transactions written. Start from an order and you can walk to the exact credits that funded it; start from a wallet and you can walk to every order it touched. Auditors call this a transaction trail. We call it Tuesday.

But the real reason to be API-first is that the storefront turns out to be just one client among many.

API-first by design: the storefront is just one client of the wallet, alongside admin, automations, finance, an AI agent and the physical world.

The admin portal issues, tops up, suspends and adjusts through the same APIs. The automation engine is a client too: a starter pack issued the moment HR creates a new hire, annual allowances landing every July, each run idempotent so a re-run can never double-issue. The reporting layer reads the same records. We've even wired an AI admin agent to the same endpoints, so an administrator can type "issue Dan a $200 starter pack for the Melbourne team" and the same guarded, audited write path executes it.

And because none of the intelligence lives in the storefront, the wallet doesn't stop at the website. A swipe card at a trade counter, a QR code at a fitting event, a kiosk in a distribution centre: each is just another client asking the same API the same question, "can this person have this item on this credit?" The card in someone's pocket holds no value at all. It identifies a person; the ledger does the rest, with the same rules and the same audit trail as the online store. That's the difference between building a storefront feature and building an entitlements engine.

The features we refused to build

A list of what this engine deliberately does not do, and why that's a feature: no transfers between people, no overdrafts, no negative balances, no mixing points and dollars on one portal, no customer-initiated top-ups, and no soft holds on carts (payment captures at order placement, refunds close the loop). Every one of these is a place where flexibility breeds either fraud, reconciliation pain, or a support queue. The ledger model means any of them could be added later without reshaping what exists. Constraint now, options forever.

The pattern, not the product

Strip away the uniforms and what's left is a pattern: value, issued by an organisation to a person, under rules, with a lifecycle, and a paper trail good enough for the CFO. Uniform allowances. PPE entitlements. Tradie onboarding kits. Safety boot programs. Recognition points. Different words, same machine.

That's exactly why the engine lives as a pattern and a set of APIs rather than in any one client's codebase. The lesson we'd hand to anyone building one: put a ledger at the heart, make the rules readable by the people who own the policy, make every write idempotent, and expose the whole thing as an API so the storefront is a client rather than the centre of the universe. Get those four right once, and the engine deploys into businesses you haven't met yet: the hard problems solved properly, waiting for the next brand that walks in with them.

AFKN designs and deploys B2B commerce platforms on commercetools, including the entitlements engine described here. If your business issues value to people under rules, and the spreadsheet is starting to creak, we should talk: hello@afkn.com.au