Caching and Concurrency Before Any Code Gets Written
Which two APIs in a Splitwise-style tracker are worth caching, and the exact race condition that makes mutating a shared balance map in place dangerous.
The last post said the next post turns the algorithm into real TypeScript.
That was too early. Two more design decisions need answering first: which APIs are worth caching, and what happens to that cache when a read and a write happen at the same time. A later post gets to the code, after one more on architecture.
Not Every API Deserves Attention
A Splitwise-style tracker has a bunch of simple APIs: get a user, get a group, get one expense. These do not need any design thought.
In an LLD interview, that is the point. You are not being tested on whether you can fetch a record by ID. You are being tested on the parts that actually have nuance: an algorithm, a caching decision, a concurrency bug.
So skip the simple APIs entirely and go straight to the two that matter.
The Two APIs Worth Caching
You cache something when many people ask for it and computing it is expensive. In this tracker, two APIs fit that description.
getGroupExpenses: every member of a group calls this constantly, and it has to gather every expense tied to that group.getGroupPaymentGraph: this is the settlement result described in the expense object post, the actual list of who pays who. It is also expensive, since it means summing every balance and then running the greedy heap algorithm on top.
Both get hit repeatedly by the same group of people, and both cost real work to compute. That combination is exactly what caching exists for.
Building the cache itself, picking an eviction policy, deciding how big it should be, is a separate problem. Name it out loud as a consideration and move on. Implementing an LRU cache is not what this interview is testing either.
The Problem That Actually Matters: A Race Condition
Here is the situation. Somebody is reading getGroupPaymentGraph from the cache at the exact moment somebody else edits a group expense.
Can that reader see a broken, half-updated result?
Picture one expense's balance map before anything changes: A owes 10 rupees, so A's balance is -10. B is owed 10 rupees, so B's balance is +10. That pair always has to sum to zero, since every rupee someone owes is a rupee somebody else is owed.
Now say that expense gets edited, and the new amount is half of the old one. The correct updated map is A at -5, B at +5. That still sums to zero.
The danger is in how you get from the old map to the new one.
If you update A's field and B's field one at a time, in the same shared map, there is a window where the update is half-done. Walk through what can go wrong in that window.
- B's field updates first: B goes from +10 to +5.
- Before A's field finishes updating, a read comes in.
- That read sees A still at -10, since A has not been touched yet, and B already at +5.
The reader gets A: -10 and B: +5. That sums to -5, not zero.
That is not stale data. It is not even real data. It is a state the balances were never actually in, at any point in time, for anyone. An app returning that to a user is returning nonsense.
ExpandA race between an in-place field update and a read: mutating in place can hand back a half-updated, impossible balance, while building a new map and swapping the reference always hands back one complete, consistent map
The Fix: Build New, Then Swap the Reference
The fix is to stop editing the existing map at all. Instead, build a brand new map with both updated values already in it, A at -5 and B at +5, and only then point the reference at that new map instead of the old one.
A reader now only ever has two possible outcomes, never a mix of the two.
- They read before the swap, and get the old map: A at -10, B at +10. Stale, since the edit had not landed yet, but internally consistent, since it still sums to zero.
- They read after the swap, and get the new map: A at -5, B at +5. Current, and also consistent.
Either way, the map they get is a real map that existed as a whole at some point. There is no in-between moment where a reader can see it.
This pattern has a name: eventual consistency. A reader might get an answer that is a step behind. It will never get an answer that makes no sense.
The other option is a write lock, blocking every read until a write finishes. That works too, but it means readers wait. Swapping an immutable reference gets you the same safety without anyone waiting.
For this to work, both getGroupExpenses and getGroupPaymentGraph need their underlying objects to be immutable. Neither can be a map that gets edited field by field. Both have to be replaced, whole, every time something in them changes.
Budget Your Interview Time Around This
In a roughly 60-minute machine coding round, requirements plus defining every object usually eats about 35 minutes. Drawing the objects on the board, explaining them, thinking out loud as you go, none of that happens fast, and the interviewer expects you to actually think rather than recite something memorized.
That leaves around 25 minutes.
Spend those 25 minutes on the genuinely hard problems: the algorithm, the caching decision, and concurrency. Not re-explaining that getUser is a simple lookup. The two APIs and the race condition in this post are exactly the kind of thing that 25 minutes needs to go toward.
The next post in this chapter covers the architecture decision that comes before any code: how these pieces, the algorithm, the cache, the immutable maps, actually fit together as classes.
Keep reading