Retail & consumer

One customer record behind the till, the app and every campaign

A real-time billing feed, a campaign manager marketing can run without an engineering ticket, and a recommendation engine that still works on a customer's first visit.

Campaign launch, brief to live
weeks → same day
Recommendation click-through
roughly tripled
Profiles per real customer
several fragmented records → one
Sector
Multi-category retail chain
Estate
180 stores plus app and web, ~2.4m loyalty members
Engagement
Discovery sprint, then delivery pod
Duration
12 months, phased by module

Stack

  • Kafka
  • Snowflake
  • dbt
  • Redis
  • LightFM
  • FastAPI
  • React
  • Airflow
  • Metabase

Practices involved

Discuss a similar problem

The situation

Store billing ran on regional point-of-sale systems that only reconciled overnight, loyalty points sat in a legacy add-on that marketing could not touch without filing an engineering ticket, and personalisation meant a spreadsheet segment emailed to whoever ran that week's push notification. The same person could appear as three or four different customer records depending on whether they had paid in-store, used the app, or scanned a loyalty card, and nobody could say with confidence which number in which system was the real one.

The constraint

The counter could not go down. A billing migration that breaks the printer at checkout is a P1 incident in any retailer, not a bug ticket. Loyalty rules also change constantly — seasonal multipliers, tier bonuses, expiry — and marketing needed to launch them without a release, but every rule is also a financial liability, since a point is a promise of a future discount, so self-service could not mean unsupervised. And a large share of transactions are still walk-in or a customer's first loyalty scan, so recommendations had to be useful with no purchase history at all, not just for the well-known regulars.

What we built

One customer identity across till, app and loyalty card

Deterministic matching on card and phone number, probabilistic matching where those are missing, and a documented survivorship rule for what happens when two records disagree. This is the same identity discipline we apply to reader accounts in publishing or patients in a clinical system — a merge rule someone can explain, not a fuzzy join nobody wants to look at.

Billing as an event stream, not a batch export

Store bills stream as structured events in near real time, replacing the nightly export that used to mean today's numbers were not available until tomorrow. The migration ran store cohort by store cohort, with the legacy export kept live as a fallback until each cohort was verified.

A campaign manager marketing can actually run

A rule-based points and campaign engine with a UI for defining tiers, multipliers, expiry and targeted offers, gated by a dry-run mode that projects the point liability before anything goes live, and a staged sign-off for rules above a threshold. Marketing gets to move fast on the rules that cannot hurt the business, and a checkpoint on the ones that can.

A recommendation engine that does not need history to be useful

Collaborative filtering for customers with a purchase record, an association-rule fallback for first-time or anonymous customers, refreshed often enough to reflect this week's stock and promotions rather than last quarter's.

Serving inside the time a bill takes to print

A cached, precomputed candidate layer means a next-best-offer or a points nudge resolves within the checkout flow's latency budget, so the offer is on the receipt or the screen before the customer has already turned to leave.

What changed

A campaign that used to take weeks from brief to launch now ships the same day, with the liability known in advance rather than discovered in the numbers afterwards. Recommendation click-through roughly tripled, and the average customer went from several fragmented profiles to one.

What we would do differently

The liability simulator shipped after self-service launch access did, not before. In month two, marketing launched a multiplier campaign more generous than intended, and the gap between projected and actual liability was not caught for a week. The simulate-before-launch guardrail should have gated the first self-serve campaign, not the fiftieth.

Outcomes

Campaign launch, brief to live
weeks → same day
Recommendation click-through
roughly tripled
Profiles per real customer
several fragmented records → one

Client identity withheld under a mutual NDA. Figures are illustrative — rounded and directional, meant to show the shape of the change rather than an audited result. We will walk through the real numbers, and how they were measured, under NDA on a call.

Next step

Tell us what you're trying to ship.

Send the brief, the RFP, or three messy sentences about the problem. You get a written point of view from an architect within two working days — not a sales deck.