Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

loyalty-ledger

An append-only loyalty point ledger with derived balances and country-specific reward rules.

A small, focused demonstration of one idea: the ledger is the source of truth, and balances are computed - never stored and mutated. This is the same pattern behind the loyalty wallet I built for GlamGuider, a consumer platform with 90K+ registered users and 80K+ active wallets; this repo extracts the core design into ~150 lines you can read in five minutes.

The core idea

Most naive point systems store a balance number on the user and add/subtract from it:

user.balance += 50;   // fragile

The problem: if anything goes wrong - a race condition, a half-applied transaction, a bug in one code path - the stored balance silently drifts away from reality, and you can't tell why a balance is what it is.

This ledger never does that. It stores an immutable list of transactions and derives the balance by summing them:

balanceOf(userId) =  sum of every transaction for that user
event → rule engine → grant log (append-only) → derived balance → wallet API

Consequences of this design:

  • Auditable - every balance can be explained by replaying its history.
  • Correct by construction - there's no separate balance to drift out of sync.
  • Append-only - mistakes are fixed by adding a compensating entry, never by editing history (the same principle as accounting double-entry or event sourcing).

Country-specific rewards

Reward amounts depend on the user's country, kept as data rather than scattered conditionals - so adding a country is a config change, not a code change:

IN: { signup: 50,  review: 10, referral: 100 }
US: { signup: 100, review: 20, referral: 200 }
// unknown countries fall back to DEFAULT

In production this is exactly how multi-region reward rules stay maintainable: the rule table grows, the engine doesn't change.

Run it

npm test     # 8 passing tests
npm run demo # see the ledger in action

Example demo output:

alice balance: 70           # IN: signup(50) + review(10) + review(10)
bob balance: 120            # US: signup(100) + review(20)
alice redeems 40 → balance: 30
alice redeems 1000 → blocked: Insufficient balance

audit trail:
  +50  (earn:signup:IN)
  +10  (earn:review:IN)
  +10  (earn:review:IN)
  -40  (discount-coupon)
Balance is the sum of the trail above - never stored, always derived.

The one invariant the ledger enforces

A balance can never go negative. redeem() checks the derived balance before appending a debit and refuses if there aren't enough points. Everything else is just credits and debits on an immutable log.

How this maps to production

In this repo In production (80K+ wallets)
transactions array append-only DB collection (no UPDATE/DELETE)
balanceOf() summing in memory same query, with periodic balance snapshots for performance
frozen transaction objects DB-level immutability constraints
in-process call a service behind an API, one transaction per request

At high volume you add balance snapshots (store a checkpoint every N transactions and sum only entries after it) so you don't replay all history on every read - but the source of truth stays the log. A streak & milestone engine sits on top of the same grant log in production: streaks are just queries over the event history, which the ledger already preserves for free.

Files

src/ledger.js       - the append-only ledger + derived balance
src/rules.js        - country-specific reward rules (as data)
test/ledger.test.js - 8 tests covering earning, redeeming, audit trail, immutability
demo.js             - runnable walkthrough

Built by Mahesh Konar as a focused demonstration of ledger design. MIT licensed.

About

Append-only ledger with derived balances — the pattern behind a production loyalty wallet with 80K+ users. Runnable + tested.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages