Customers judge a loyalty programme by its balance. When the points from today's burger don't appear, show up late, or a reward is declined at the counter, the app and the offers lose credibility, however well the launch went.
Accounting standards treat the balance seriously too. Under IFRS 15, points that give a customer a material right are a separate performance obligation. The customer has in effect paid in advance for future goods, so part of the transaction price is allocated to the points and held as a contract liability until they are redeemed or expire (European Commission, 2016, IFRS 15 para. B40). That allocation already reflects how likely the points are to be used (para. B42). Points that are never redeemed are often referred to as breakage. A business that expects breakage recognises it in line with the pattern of redemptions, and one that doesn't recognises it when redemption becomes remote (paras. B45 to B46). US GAAP's Topic 606 was issued alongside IFRS 15 with the aim of reaching the same conclusions (FASB, 2014). Either way, finance depends on a reliable redemption history, and that history comes from the loyalty backend.
That puts a financial system behind a marketing programme, and promotions are where the two pull apart, because a double-points weekend changes the rules and the volume at once. Accrual is one of the workloads Peak Load Is the Steady State treats as able to queue under pressure, which is safe when the backend is designed for it. The stages below follow one point through that design.
Enrol
The customer signs up in the app, at a kiosk, or by giving an email address at the counter. The data engineering question here is identity. The same customer may enrol twice, once in the app and once at a kiosk, or may already exist in the CRM from an earlier programme. If the backend creates a new member for each, points get split across records, and the customer sees a lower balance than they expect. Resolving identity at enrolment, by matching on verified contact details and linking payment tokens (never card numbers) or app installs to one member, is usually simpler than merging accounts after points have been earned on both.
Enrolment is also where the basis for using the member's data is recorded. Loyalty programmes have rules of their own. California allows financial incentive programmes, which include many loyalty schemes, but a business can enter a consumer into one only with prior opt-in consent that describes the material terms and can be revoked at any time (Cal. Civ. Code §1798.125(b)(3)). Under GDPR, a member can object at any time to direct marketing, including related profiling, after which their data can no longer be used for that purpose (European Parliament and Council, 2016, Art. 21(2) to (3)). Those choices have to travel with the member record, because the offer engine depends on them.
Earn
The customer buys something and scans their app, or the POS links the payment to their member ID. This creates an earn event, and it's where a loyalty backend first meets the realities of a restaurant or store network.
Stores don't always have a reliable connection. Many POS systems keep trading when the link drops and forward their transactions when it comes back, so earn events can arrive late, in bursts, and out of order. Transactions get voided, refunded, or amended after the fact. Some brands run more than one POS across their network, each with its own event format. Members also buy through several channels: an app order paid in the app and collected at a store that's offline, a kiosk that identifies members by QR code, or a delivery-aggregator order that arrives through a separate integration. Receipt-claim features, where a customer enters a code later, add earn events that are late by design.
A backend that treats each incoming message as a fresh instruction to add points will double-count retried messages and miss refunds. The usual fixes come from other event-driven systems. Every earn event carries a key the source can reproduce on retry, often a composite of store, register, business date, and transaction number, because POS transaction IDs can repeat across stores. Voids and refunds reverse the original even when the points have already been spent, so the backend needs a rule for negative balances. Late events are applied in transaction order, and order-dependent rules, such as a bonus on the fifth visit, are recalculated when a late event lands earlier in the sequence.
Points have cash value, so the event stream also carries abuse: staff attaching unclaimed transactions to their own member ID, accounts opened in bulk to farm a sign-up bonus, and takeovers of accounts with large balances. These tend to show up as patterns of velocity, device, and store in data the backend already processes, so fraud rules can often run on the accrual stream before points become redeemable.
Accrue
Accrual is where earn events become points, by applying the programme's rules: the base rate, the bonus, the double-points weekend, the offer the customer activated in the app. A successful promotion raises volume and rule complexity together, which is why accrual is often where a backend falls behind.
What matters most to customers is what the POS has to wait for. Identifying the member and checking which offers apply usually has to happen while the basket is open, because a discount changes the price. Posting the earned points usually doesn't. If the POS waits for accrual before completing the sale, a slow rules engine becomes a slow counter. If it writes the transaction to a durable queue and moves on, the counter stays fast and accrual can catch up after a spike. The synchronous path is then limited to member lookup and offer validation, ideally with a cached fallback.
Catching up is acceptable when customers can see what's happening. A balance that shows points as pending, with the transaction that earned them, is likely to hold trust better than a balance that shows nothing with no explanation. Treating accrual as a stream processing problem, with a queue that absorbs the burst, a rules engine that scales out, and a lag metric someone watches, means a promotion spike shows up as pending points, not missing ones. For restaurant and retail brands, our Data & AI practice often starts with the accrual pipeline and its lag metric, because that's where a promotion shows up first.
Redeem
Redemption is usually the hardest stage, because it has to be right at the moment it happens. A customer at the counter wants a free coffee with their points. The POS asks the backend whether the balance covers it, and if so, the points have to be deducted before the coffee is handed over.
Two things can go wrong. If the balance the POS sees is behind accrual, a customer who has the points may be told they don't. If two redemptions happen close together, at the counter and in the app, a backend without a reservation step can let both through against the same points. Redemption needs a consistent view of the balance and a reservation that holds the points until the sale completes or is cancelled, so it often gets its own path through the backend, separate from the bulk accrual pipeline.
Stores that keep trading offline face the same question at redemption. Some brands block rewards until the link returns, and others allow limited offline redemptions and reconcile afterwards. That's a trade-off between fraud exposure and goodwill, and the backend has to record which path each redemption took.
Offers add another layer. Rewards are often limited by store, time of day, product, or quantity, and each limit is a rule the POS and the backend have to agree on. Where they disagree, the customer sees a reward in the app that the counter won't honour, which can undo some of the goodwill the promotion created.
Expire
Many programmes expire points after a set period or a stretch of inactivity. For finance, expiry releases the liability held against those points (European Commission, 2016, IFRS 15 para. B40). For the backend, it's a scheduled job that runs against the event history, sends any reminder the programme's terms promise, and leaves a record that reconciliation can match. Expiry rules tend to change over a programme's life, so the backend has to know which rule applied to which point.
Reconcile
Each trading day, the loyalty ledger has to agree with the systems around it: the POS sales records, the payments data, and the finance team's liability account. Points issued should match qualifying transactions. Points redeemed should match the rewards given away at the counter. The outstanding balance should support the liability on the balance sheet, and the redemption history feeds the breakage estimate the auditors will ask about.
Franchising adds another layer. In a franchise network, the store that issues points and the store that redeems them may belong to different franchisees. One common approach is a set reimbursement rate per reward, often funded from the marketing fund, and calculating it needs a store-level record of every earn and redemption event.
A backend built with reconciliation in mind keeps the event history as the source of truth and derives balances from it, so any balance can be explained transaction by transaction. A running balance that's updated in place can be hard to reconcile after a busy promotion, because the history behind each number has to be reconstructed.
Across the network
Each stage tends to get harder across a large network. Hundreds or thousands of locations mean several POS versions in the field at once, different connectivity, different trading hours, and in many networks a mix of company-owned and franchised stores with their own systems.
Loyalty data tends to be most useful in the same platform as sales and operational data, so marketing, operations, and finance work from one record. Sakura's Google Cloud data platform for Craveable Brands, the franchisor behind Red Rooster, Oporto, and Chicken Treat, brought loyalty and CRM records together with point-of-sale, marketing, and franchisee data across what was then more than 570 restaurants (Craveable Brands case study). Part 2 of this series looks at running a network of sites on one pipeline.
After go-live, the lag alert, the daily reconciliation, and the POS adapter that breaks after a vendor release all need an owner. Our Managed Services team takes that on for restaurant and retail brands.
References
California Legislature, n.d. California Civil Code §1798.125 (California Consumer Privacy Act). California Legislative Information. Available at: https://leginfo.legislature.ca.gov/faces/codes_displaySection.xhtml?lawCode=CIV§ionNum=1798.125 [Accessed 6 October 2026].
European Commission, 2016. Commission Regulation (EU) 2016/1905 of 22 September 2016 amending Regulation (EC) No 1126/2008 adopting certain international accounting standards in accordance with Regulation (EC) No 1606/2002 of the European Parliament and of the Council as regards International Financial Reporting Standard 15. Official Journal of the European Union, L 295, 29 October. Available at: https://eur-lex.europa.eu/eli/reg/2016/1905/oj [Accessed 6 October 2026].
European Parliament and Council, 2016. Regulation (EU) 2016/679 of the European Parliament and of the Council of 27 April 2016 on the protection of natural persons with regard to the processing of personal data and on the free movement of such data (General Data Protection Regulation). Official Journal of the European Union, L 119, 4 May, pp. 1-88. Available at: https://eur-lex.europa.eu/eli/reg/2016/679/oj [Accessed 6 October 2026].
FASB, 2014. Accounting Standards Update No. 2014-09, Revenue from Contracts with Customers (Topic 606). Financial Accounting Standards Board, May 2014. Available at: https://storage.fasb.org/ASU%202014-09_Section%20A.pdf [Accessed 6 October 2026].
Top comments (5)
the distinction between the synchronous path (member lookup/offer validation) and the asynchronous path (accrual via durable queues) is a masterclass in distributed systems design. treating accrual as a stream processing problem with a "pending" state is exactly how you preserve user trust during peak load without blocking the pos.
your point about idempotency keys being a composite of store, register, business date, and transaction number is spot on. pos transaction ids repeating across different stores is a classic trap that catches many engineers off guard.
curious about one edge case in the reconciliation layer: when a franchisee's local pos goes offline for an extended period, processes transactions locally, and then syncs a massive batch of events days later, how do you handle reconciliation conflicts? do you rely on strict vector clocks/timestamp resolution, or does the central ledger act as the ultimate arbiter and force a manual review for anomalies that exceed a certain threshold?
massive respect for grounding this in ifrs 15 and gaap. reminding engineers that "points are a contract liability, not just marketing fluff" is the exact mindset shift needed to build systems that don't collapse under audit. 🐯
Thanks, glad it was useful, and that’s a good edge case.
In the designs we’ve built, there’s a bit of both, with each mechanism doing a different job.
Ordering and duplicates: We usually don’t need vector clocks, because each register is effectively a single writer. The composite key (for example store, register, business date, transaction number) handles duplicates. Where the POS provides a register sequence number, it also lets the backend spot gaps, so a batch that arrives days later can be checked for completeness before it’s applied. Events are applied in transaction time, so a late batch slots into the member’s history where it belongs. Anything that depends on order, like a fifth-visit bonus or points that would have expired in the meantime, gets recalculated from that point.
The ledger as arbiter: The event history is the source of truth, and balances are derived from it. Late data never overwrites anything; it just adds events. Earns from the late batch post normally. The difficult cases are the ones where the member did something else in the gap, usually redeeming points online that the offline store was about to reverse, or the offline store allowing a redemption it couldn’t validate. That’s where the negative-balance rule and the record of which redemption path was taken come in.
Thresholds and manual review: Automatic handling covers most of it. Anything outside agreed limits goes to a queue for a person to review. That includes batches older than a set number of days, or a batch whose totals don’t match the store’s end-of-day report. It also includes balances driven negative beyond a tolerance, or point values high enough to look like abuse.
Finance: If the period the transactions belong to has already been closed, we don’t reopen it. The correction is booked in the current period as an adjustment, and the record keeps both the original and the corrected view. For franchisees, that also feeds the reimbursement calculation.
Part 2 of the series goes into the store-day side of this: marking days as provisional until they reconcile against the end-of-day report, and keeping revision history. sakurasky.com/blog/qsr-retail-engi...
andrew, this is a masterclass in enterprise data engineering. the "ledger as arbiter" pattern is exactly the right approach. treating the event history as the immutable source of truth, where late data appends rather than overwrites, is the only way to maintain true auditability.
i especially appreciate the detail on financial adjustments for closed periods. booking the correction in the current period rather than reopening a closed ledger is a mature, real-world pattern that many basic tutorials completely gloss over.
in constraint-driven environments (like spotty 3g networks where "late batches" are the norm, not the edge case), this robust append-only design isn't just a best practice—it's a survival mechanism. it prevents data corruption when syncs inevitably fail or delay.
thanks for sharing part 2 of the series. the depth of this qsr retail engineering breakdown is exactly the kind of high-signal content the community needs. 🐯
Bài viết chạm đúng điểm đau: loyalty không chỉ là CRUD đơn giản mà là bài toán eventual consistency ở scale. Thấy nhiều team vẫn dùng transactional DB để lưu point balance rồi query real-time — ngon cho MVP nhưng sập khi peak hour (Black Friday, flash sale) vì lock contention trên row
user_id.Một pattern đã giúp đỡ: event sourcing + CQRS. Ghi
PointEarned,PointRedeemed,PointExpiredvào append-only log (Kafka/Pulsar), projection riêng cho read model (balance view) — cho phép replay, backfill, debug không đụng production. Idempotency key trên mỗi event là bắt buộc (duplicate webhook từ POS, retry network).Thêm vào đó: reconciliation job hàng đêm so sánh
sum(events)vsbalance_snapshotđể catch drift sớm. Không có job này thì bug chênh lệch 1-2 point sẽ lăn thành incident support hàng ngàn ticket.Curious: team bạn handle
PointExpirednhư thế nào? TTL trên Redis hay scheduled job scan DB? (site: labagent .tech)Thanks for reading. We agree on idempotency keys and on checking the sum of events against stored balances regularly. Part 1 makes the event history the source of truth for the same reason.
On expiry, in the designs we’ve built it’s neither of those exactly. A TTL deletes the balance silently, so there’s no event to replay or audit, and nothing to explain to a member who asks where their points went. A scan of the balance table has a similar gap. Instead, a scheduled job works out what has expired from the event history and writes an expiry event for it, recording which rule applied. The expiry then replays, reconciles, and reaches finance like any other event. That matters for revenue recognition too, since expected expiry (breakage) is part of how the points liability is estimated.