The TV

I got a TCL TV with Fire TV on Amazon Prime Day last year(2025). It’s a decent deal for the price, but the CPU is noticeably underpowered for the operating system. Most of time it’s functional enough that I appreciate it’s existence. Sometimes though, I want to throw a brick at it. The most infuriating case is when I browse Netflix, land on an interesting show, and press “OK” to see more details. Nothing happens. So I press it again. Then, after some silence and janky animations, the show starts playing, and then I’d struggle to go back, and then Netflix would mark that show as something I started watching. A show I never intended to watch becomes a forever mark in my identity, according to Netflix.

A Distributed System

And when I think about this, the root cause is actually a classic replication failure. There are two nodes in the system, me and the machine. And our state were out of sync. You may disagree with this, and argue that it’s ridiculous to model human beings as nodes in a system. However, the way I think about it, is that whenever the rate of action in a system is faster than how fast a node, who acts based on local state, can keep up, the problems and challenges of a distributed systems emerges. This applies to computer to computer systems, computer to human systems, and even human to human systems(teams, organizations, etc.) too.

Web Apps Are ALWAYS Distributed Systems

By this definition, web apps are always distributed systems, because, well, there’s always lag between the client and server, other users might mutate state, background processes on the server can mutate state, time is always passing, etc.

Here I’m assuming there’s no lag between human and the client, user’s actions are timely, relative to user’s rate of action, reflected on their screen.

In a distributed system, you can never safely assume that one node(the client)’s world state matches the other(server)’s. And yet, the vast majority of APIs are designed as if this isn’t true. They accept an action, which is generated by the client based on client’s state, execute it against current server state, and return a result or an error, leaving the client’s assumptions unvalidated. You would not do this in a distributed backend system.

This is the gap I want to talk about: the client’s observed world state is an implicit input to every API call, but most APIs never ask enough of it.

What This Looks Like in Practice

Consider a checkout API:

POST /checkout
{
  "cart_id": "12345"
}

This tells the server what to do, but nothing about what the client believes to be true when asking. If the user had the cart open in two tabs and added an item in the other one, this request will silently checkout with items the user may not have intended to buy. If the price of an item changed after the user added it to their cart, they might be charged more than they expected.

A state-aware version changes the contract:

POST /checkout
{
  "cart_id": "12345",
  # assume the cart version updates whenever the cart item changes or price updates
  "cart_version": "xxxx"
}

Now the server can validate: “is this the cart state the client was looking at?” If not, it rejects the request with a meaningful error instead of executing against a world the client didn’t see.

There are subtler examples too. Consider this API that’s called as the last step of enabling a paid feature during onboarding:

POST /enable_feature
{
  "feature_name": "new_cool_feature"
}

On the backend, whether this generates an invoice depends on the user’s current plan. If the client doesn’t declare what it thinks will happen, you can end up charging a user who believed it was free, or skipping a charge for someone who agreed to pay. In cases where the user is acknowledging a pricing contract, it leads to financial or legal damage, because the client and the server may have different understandings of what the user agreed to to.

A better design would look like this:

POST /enable_feature
{
  "feature_name": "new_cool_feature",
  "acknowledged_tos": "paid_plan_tos_v3",
  "acknowledged_pricing": "paid_plan_pricing_v3"
}

The server now validates that the client’s understanding of the situation matches reality before doing anything irreversible.

Some Existing Practices Are Already Doing This

None of this is entirely new, the industry has developed several patterns that are, at their core, ways of encoding client world state into requests. They just haven’t been unified under this framing.

Idempotency keys are a client’s way of saying “this is what I am naming my world in which this operation is to be performed.” The server can use this to detect and reject duplicate requests from a stale view. (Although in practice, this can be implemented in a way before a request reaching the business logic or even before the app layer, but it still fits in the frame of one node claiming a world state to another node.)

Base version checks in version control work the same way. When you push a commit, you’re declaring what branch state you were working from. If someone else pushed in the meantime, your assumed base no longer matches reality, and the push is rejected - saving you from silently overwriting their work. This also allows the server to return useful error messages like “push rejected because your branch is out of date, you can force push or pull and merge first”.

Authentication headers are also a form of world state declaration: “I am this identity, and I expect you to treat this request through that lens.” The server validates the claim before proceeding.

Each of these is solving the same underlying problem in a narrow domain. What I’m arguing is that expressing client world state should be a first-class concept in API design, applied broadly and deliberately rather than as a case-by-case patch.

Designing for State Divergence in APIs

Treating world state as an explicit input has a few practical consequences for API design.

When the declared state doesn’t match server state, the API can return a specific, actionable error instead of silently succeeding against a world the client didn’t see, or throwing a non-actionable, generic error message. This is the difference between “checkout failed because your cart was updated” and a mysterious wrong order or a geneic “failed, please try again”.

It also forces a useful discipline on the API designer. Before finalizing any endpoint, ask:

  • What’s the user intention for an API call?
  • What does the client need to believe for this intention to make sense?
  • What’s a good way to encode the intention and the client’s belief in the API contract?
  • If client and server’s world view have diverged, what’s the worst thing that silently happens?
  • What error can I return that helps the client or user understand what happened and how to fix forward?

At the end of the day, every web app is a distributed system. We spend lots of time designing for distributed state on the backend, but we often forget that the client, and the human is part of that system too.

PS: if you’ve ever worked on any web app, you should confidently state on your resume that you have experience working with distributed systems. Because you do.