Cosmic Module
D
Qubits of DPK
April 11, 2026
Core DBMS
Domain Understanding
A Reward Points System (like Zomato Gold, Flipkart SuperCoins, or Amazon Pay Rewards) allows customers to earn points through purchases or actions and redeem them against future transactions. The schema challenge is fundamentally one of financial ledger design. Points are a currency. Every earn and burn event must be recorded with the same discipline as a bank transaction: append-only, immutable, with a running balance that can be recomputed from the ledger at any time.
The secondary challenge is that points often have expiry — points earned today may expire in 12 months if not redeemed. This means the system must track not just how many points a customer has in total, but when each batch was earned, when each batch expires, and how many of each batch have already been consumed. This is the point batch problem and it completely shapes the schema.
The third challenge is redemption ordering. When a customer redeems 500 points, which batch do you deduct from? FIFO (use the oldest points first, closest to expiry) is the fair approach and the industry standard, but it requires the schema to track per-batch balances.
The system is both write-heavy (every order generates an earn transaction) and read-heavy (every checkout page must show the customer's current redeemable balance).
Entity Map
javascript
QUBITS OF DPK
Full Schema with Comments
sql
QUBITS OF DPK
Key Design Decisions
The FIFO Redemption Algorithm
When a customer redeems 500 points, the system must consume from the oldest batches first (FIFO) to be fair — use the points closest to expiry before using newer ones. Here is how the schema supports this:
sql
QUBITS OF DPK
The idx_batches_account_status composite index on (account_id, status, expires_at) is what makes this query fast. Without it, finding redeemable batches would full-scan the entire point_batches table.
Points Expiry Background Job
A scheduled job runs daily (or hourly for precision) to expire points that have passed their expires_at:
sql
QUBITS OF DPK
The idx_batches_expires index on (expires_at, status) is specifically for this job. Without it, the job scans the entire table every run.
Indexing Strategy
Common Traps to Avoid
- Storing balance as the only truth — If balance_points is the only record, you cannot audit how the balance was reached, you cannot reverse individual transactions, and you cannot answer "how many points did this customer earn in Q3?". The ledger is the truth; balance_points is just a cached total.
- No batch tracking for expiry — Without batches, you know total balance but not which points expire when. You cannot implement FIFO, you cannot warn customers "200 points expire next week", and you cannot run accurate expiry jobs.
- Updating existing transaction rows — Transactions are financial records. Updating them destroys the audit trail. Every correction is a new compensating transaction (ADJUST type).
- No reward_rules table — Hardcoding earn rates in application code means every promotional rate change requires a code deployment. A configurable rules table gives marketing teams control without engineering involvement.
- Not wrapping redemption in a transaction — Decrementing batch remainders and updating balance_points must be atomic. A failure between these steps leaves the system in an inconsistent state where points were drawn from batches but the account balance is wrong.
Interview Q&A
Q: Why model points as a ledger instead of just a balance column?
Because points are a currency. A balance column answers "how many points do you have?" but cannot answer "how did you get them?", "which can I redeem?", "which expire next week?", or "what happened on this disputed transaction?". The append-only ledger provides a complete, auditable history from which the balance can always be recomputed. balance_points is just a cached sum for performance.
Q: How do you handle point expiry?
Earn transactions create point batches with individual expiry timestamps. A scheduled job runs periodically, queries batches where expires_at <= NOW() AND status IN ('ACTIVE', 'PARTIALLY_USED'), inserts EXPIRE transactions into the ledger for the remaining points, and updates the batch status and account balance. All inside individual transactions per batch for atomicity.
Q: What is FIFO redemption and why is it important?
FIFO means consuming points from the oldest batches first — using points closest to expiry before newer ones. This is fairest to the customer because it minimises unintentional expiry loss. The schema supports FIFO by ordering the batch query by expires_at ASC during redemption.
Q: How do you handle the case where a customer returns an order and wants their redeemed points back?
Insert a REFUND_REDEEM transaction with +N points (restoring the redeemed amount) and a REFUND_EARN transaction with -N points (reversing the earned points from the original purchase). Recreate the batch that was exhausted by the redemption, or add back to the partially-consumed batch. All of this is new ledger entries — no existing rows are modified.
Q: How do you prevent a race condition where a customer redeems points twice simultaneously?
Use SELECT FOR UPDATE on the point_accounts row at the start of the redemption transaction. This acquires an exclusive row lock, blocking any concurrent redemption for the same account until the first transaction commits. Combined with a CHECK (balance_points >= 0) constraint or an application-level guard, double-spend is prevented.
ANKI Cards
Q: Why is an append-only ledger the correct model for a reward points system instead of a single balance column?
A: A balance column only tells you the current state. A ledger records every movement, enabling full auditability, dispute resolution, per-transaction reversal, expiry tracking per earn batch, and balance recomputation from history. balance_points is a performance optimisation on top of the ledger, not a replacement for it.
Q: What is the FIFO redemption problem in reward points, and how does the schema solve it?
A: When a customer redeems points, the fairest approach is to consume from the batch nearest to expiry first (FIFO), minimising accidental expiry loss. The schema solves this with point_batches, each carrying expires_at and remaining_points. The redemption algorithm queries batches ORDER BY expires_at ASC and draws them down in order, recording each draw in batch_redemptions.
Q: In a reward points schema, what does a signed points column on point_transactions enable?
A: A single column with positive values for credits (EARN, REFUND_REDEEM) and negative values for debits (REDEEM, EXPIRE, REFUND_EARN) makes the ledger self-balancing. SELECT SUM(points) FROM point_transactions WHERE account_id = X gives the exact current balance. It mirrors the debit/credit model in financial accounting.