Stories · Season 3 — Magpie · Episode 06

The Customer View That Was Always a Bit Wrong

In which a customer transfers money, immediately checks their dashboard, and the dashboard lies to their face.

The story

Magpie’s home screen showed a tidy summary: total balance across all your pots, your linked card’s recent spend, and any transfers in flight — one clean view, stitched together from three different parts of the system (accounts, cards, and transfers).

Two complaints kept landing. The first: a customer would move £200 between pots, land back on the home screen a half-second later, and see the old totals. The money was right in the account, but the summary view hadn’t caught up, so it looked like the transfer had vanished. Customers would panic and transfer again — sometimes the source of a second support ticket.

The second complaint was subtler. The summary occasionally showed numbers that didn’t add up across the three sources — a transfer counted as “in flight” while the destination pot already showed it as arrived, because the three pieces were updated by separate, independently-lagging processes that never agreed on a single moment.

Kemi summed it up: “Our overview is built from three streams that each tell the truth, but never at the same instant. So the combination is always a little bit wrong.” For a money app, “a little bit wrong, briefly” is still the kind of wrong that makes people stop trusting the number.

Why this is hard the traditional way

Two different problems are hiding here. First, read-your-own-writes: a user who just did something expects to see it immediately, but most read models are updated eventually, a beat behind the write — so the freshest, most important moment is the one most likely to look stale. Second, cross-entity consistency: a view that combines several sources (accounts + cards + transfers) is only correct if those sources are combined at a coherent point; update them by separate lagging jobs and the assembled view can show states that never actually coexisted.

Hand-rolling around this means caching tricks, manual “optimistic UI” patches, and fragile joins across read models that were never meant to agree — each a new way to be subtly wrong.

How Relay changes the ending

Relay gives you two matching tools. Inline projections update a read model synchronously, inside the same transaction as the change — so the moment the transfer commits, the view reflects it, and the customer’s immediate refresh shows the truth. Multi-stream projections build a read model from several aggregates’ events into one coherent view, so the cross-entity summary is assembled deliberately rather than emerging from three races.

// Inline: the summary updates in the same transaction as the transfer.
// The customer who refreshes a half-second later sees the new totals — no lag.
public sealed class AccountSummaryProjection : IInlineProjection<FundsTransferred> { /* ... */ }

// Multi-stream: one view fed by accounts, cards, AND transfers — coherently.
public sealed class HomeScreenProjection
    : IMultiStreamProjection<AccountEvents, CardEvents, TransferEvents> { /* ... */ }

Now the home screen is right when it matters most — right after the customer acts — and the combined view is built from the streams on purpose, not by hoping three lagging jobs happen to line up. Nobody transfers twice because the first one “disappeared.” The number stops lying.

What it costs you to ignore this

  • Stale-right-after-write reads erode trust. The customer who just acted is the one most likely to be shown the old state.
  • Panic actions create real bugs. “It didn’t go through” leads to duplicate transfers and duplicate tickets.
  • Multi-source views drift incoherent. Combining independently-lagging read models shows states that never truly coexisted.
  • You patch it with fragile tricks. Optimistic-UI hacks and cross-model joins are extra surface area that’s wrong in new ways.