A shop owner does not stop selling because your server is unreachable.


That sentence sounds obvious and almost nobody builds as though it is true. Most software treats a missing network as an error state: a spinner, a toast, a retry button, and a transaction that never happened. In a market where connectivity is reliable, that is a minor annoyance. In a market where it isn't, it is the end of your product — because the owner still makes the sale. They write it in a notebook, the way they did before you arrived, and they stop opening your app.


This is the part that is easy to miss from the outside. Software that fails offline does not degrade gracefully into a worse version of itself. It gets replaced by paper. And paper has one property your application probably does not: it always works.


Design for the network you don't have

The instinct is to take an online-first application and add offline support: cache the reads, queue the writes, replay the queue when the connection returns. It is a reasonable-sounding plan and it fails for three separate reasons.





So the principle, which is the whole post:


Treat the server as an eventual destination, not a dependency. A sale is an event that happened — not a request that needs permission.


Online-first software asks the server to authorise reality. Offline-first software records reality and reconciles it later. That reframe decides almost every design question that follows.


What follows from it

Four consequences, in rough order of how much trouble they save you.


  1. Record events, not state. "Sold three units at this price at 14:06" is a fact. It was true when it happened, it stays true, and it can be merged with other facts without contradiction. "Stock is now 47" is a claim about the world, and it conflicts with every other device making the same claim. Derive state from events; never let two devices assert state directly at each other.
  2. Identity is created on the device, not assigned by the server. Every record gets an identifier the moment it exists, generated locally. If the server hands out IDs, nothing can exist until the server is reachable — which is the dependency you just removed. This is the same discipline as an idempotency key in a payments system, for the same reason: it lets the same fact arrive twice without becoming two facts.
  3. Decide what genuinely needs to be strongly consistent, and accept that it's almost nothing. Prices, product names, receipt numbering — all fine locally. Stock counts converge. The expensive one is customer credit, because a credit limit is a constraint across devices, and a constraint is exactly what you cannot enforce while partitioned.
  4. Do not trust the clock. Device clocks drift, get set wrong, and get changed deliberately. If your merge logic orders events by wall-clock time, a single wrong phone clock can reorder a day's trading. Order by something you control.


The hard part is not merging. It's admitting.

Here is where most writing on offline-first goes soft, because the honest answer is uncomfortable.


Conflict resolution is usually presented as a data problem with a technical solution — last-write-wins, vector clocks, CRDTs. Those tools answer the question "which value survives." That is not the question a shop has. The question a shop has is "what should happen now, in the business."


Two tills, both offline, both sell the last unit of stock. Pick any merge strategy you like and the result is the same: stock reaches zero and two sales exist. Neither sale is wrong. Both happened. Money changed hands twice and there was only ever one unit.


No algorithm resolves that, because it isn't a data inconsistency. It is an oversell — a real event in the world, which needs a human decision: refund one customer, source another unit, or absorb it. The software's job is to notice and say so, clearly, to the person who can act. A system that silently picks a winner has not solved the problem. It has hidden a loss inside a clean-looking database, which is strictly worse than a visible mess.


The same holds for credit. If a customer bought on account at two locations while both were offline, they are over their limit and no merge strategy un-sells those goods.


Offline-first means accepting that some conflicts are facts rather than errors. The goal is to detect and surface them, not to resolve them quietly.


This is the accounting instinct again, and it is the part of my background that has transferred most directly. You do not make a discrepancy disappear. You post an adjusting entry, you show the working, and you leave a trail someone can follow in three months.


What to build first

Not sync. Sync is the easy part once the data model is right, and building it first guarantees the data model won't be.


Build the reconciliation view first: the screen that shows the owner what the system currently believes, which records haven't reached the server, and where two sources disagree — and that lets them correct it. It feels like an admin afterthought. It is actually the trust-building surface of the entire product, and the thing that determines whether the app survives its first bad week.


Then make every write idempotent and locally identified. Then sync.


One more thing, which took me longer to learn than it should have: never let the interface lie about state. Show plainly what has synced and what hasn't. There is a temptation to hide it — green ticks everywhere, keep it calm, don't worry the user with infrastructure. It is the wrong call. People working in low-connectivity conditions are far more literate about the network than designers assume; they can see the signal bars, they know the tower is down. What they cannot tolerate is software that behaved as if everything was fine and then lost a day of sales. Visible honesty costs you a slightly busier screen. Concealment costs you the customer.


The bar

The thing a paper notebook does, that is genuinely hard to beat, is work. Every time, no battery, no signal, no update, no account. It will not total your month, it will not tell you which product is bleeding margin, and it will not get you a loan — but it has never once failed to record a sale.


Until your software clears that bar, the notebook wins. And it should.



Written about the general problem of building for low-connectivity conditions. Product-specific design and implementation details are deliberately left out.