Offline behaviour
The Wi-Fi in a basement café is what it is. The guest is still waiting.
An order is written to the device before the network is touched, carries an operation id, and is replayed in order when the connection returns — so a dropped router costs a venue a delay rather than a night. The operations that must not be guessed at refuse in the cashier’s hands instead of queueing.
01
Disk before network
Taking an order writes it to the device first and sends it second. That ordering is the whole design: if the write succeeds, the order exists, and whether the network was there is a question about when it arrives rather than whether it happened. The queue is one shared implementation across the cashier, the waiter, the kitchen and the guest apps, so "is this order lost?" has one answer rather than four.
- A queued operation records its own id, the request it stands for, when it was created and how many times it has been attempted.
- The id is minted on the device at the moment the work is accepted, not by the server, which is what makes it survivable.
- The four staff and guest apps write to device storage, so the queue is still there after a cold start.
- Every read-modify-write of the queue is serialised, and a flush removes exactly the entry it handled rather than writing a whole snapshot back over whatever else arrived meanwhile.
02
Replay in order, and a limit on trying
The queue is a single strictly ordered line. When the connection returns it is drained oldest first, and a failure that might succeed later stops the run where it is rather than sending everything behind it out of order.
- A server error breaks the run and leaves the queue intact, so the order of what the kitchen receives is the order it was taken in.
- An expired session stops the run without spending an attempt, so a token that needs refreshing does not burn through the retry budget.
- A refusal — a request the server will never accept — is set aside immediately and the run continues past it, so one bad operation does not block the orders behind it.
- After eight attempts an operation is set aside too. Nothing is ever dropped: every operation ends up delivered, still queued, or in a failed list a person has to decide about.
03
The server remembers the id
A replayed operation carries the same id it carried the first time, on a header the server reads on every request. If that id has been seen, the server returns the response it gave the first time instead of doing the work again — byte for byte, so the app cannot tell a replay from the original and does not have to.
- The same id presented at a different route is refused rather than replayed, because returning the wrong stored answer would be worse than an error.
- Money has a stronger guarantee than that. A payment carries its own key backed by a unique database column, so two simultaneous attempts cannot both succeed — one of them loses the race at the database and is handed the original payment.
- Reusing a payment key for a different amount or a different check is treated as a caller bug and refused, not as a replay.
- The record of a seen operation does not expire, deliberately: a ten-minute window would have run out before a phone leaves a dead spot, which is the exact case it exists for.
04
What refuses instead of queueing
A payment that might have happened is worse than one that plainly did not. Some operations therefore fail in the person’s hands rather than being written to the queue, and the refusal is immediate and specific.
- Card, wallet and bank-transfer payments are online only, and so is a refund against one of them, because they have to reach a provider to be true.
- Opening and closing a cashier shift is online only for a different reason: neither operation carries a replay key, so a retry could open a second shift or close against a second counted figure.
- Splitting a check is refused offline, with a message that says so rather than a spinner.
- Cash movements do queue, because a cash movement carries a key with a unique column behind it and a replay is therefore harmless.
05
What the device keeps, and what it fetches fresh
Reads are a different system from writes, and what is kept on the device is decided one endpoint at a time. The rule is simple enough to hold in your head: reference data may be kept, live state is always fetched. So a tablet relaunched in a dead spot opens on the menu a waiter can work from, and never on a table state that was true an hour ago.
- The catalog, the guest menu and the kitchen station list keep a copy on the device. Open sessions, checks, shift summaries and ticket boards deliberately do not, because a stale balance is worse than a spinner.
- A saved copy expires after seven days, and a payload over a megabyte is not saved at all — specifically so a large menu cannot crowd an unsent order out of device storage.
- A saved copy is labelled as saved, so a screen can say where it came from instead of presenting week-old data as live.
- On a cold start the device copy is read first; on any later refresh the network is tried first and the copy is only a fallback. And a saved copy is scoped to the venue it belongs to, so one venue’s data cannot be read out of another’s cache on a shared device.
06
Work that failed is a decision, not a disappearance
Operations the server refused are collected in a list on the device, with the server’s own words for why. It is shown as a standing banner rather than a toast that scrolls past, and each entry can be retried or discarded on purpose.
- A retried entry goes back to the head of the line with its original id, so retrying does not risk a duplicate.
- Discarding is explicit, per entry, and it is the only way an operation leaves the system without being delivered.
- Where two browser tabs write at once, the loser is refused rather than allowed to overwrite, and an unreadable store raises an error instead of reporting an empty queue — because "empty" would be written back over a shift’s worth of orders.
07
And when the network is there
The queue is the exception, not the normal case. On an ordinary evening the venue runs on live updates: services emit events as the work happens, and one realtime layer turns them into rooms — a branch, a station, an order, a session — so the kitchen screen, the waiter’s tablet and the guest’s phone learn about the same order at the same moment instead of each asking every few seconds.
- Joining a room is authorised, so a connection cannot listen in on a branch, a station or a session it has no business in.
- One order event fans out from a single emission to the branch board, the station that cooks it, the order itself and the guest who placed it.
- A guest’s phone follows their own session and nothing else, which is what makes item-by-item status safe to show on a device the venue does not control.
- Because the screens are pushed to rather than polling, a floor of twenty tablets is not twenty devices asking the same question all evening.
Questions about a dropped connection
If an order is sent twice, does the kitchen make it twice?
No. Every operation carries an id minted on the device when the order was taken, and the server keeps a record of the ids it has already acted on. A replay of the same id returns the response the server gave the first time rather than running the work again, so a retried order comes back as the original order instead of as a second one. The record does not expire, because the whole point of it is the phone that spent an hour in a basement.
Can I take a card payment while the connection is down?
It refuses rather than queueing, which is the behaviour you want. Card, wallet and bank-transfer payments — and refunds against them — need to reach a provider to be true, so offline they fail in the cashier’s hands with a message that says a connection is required. Cash is different: a cash movement carries a key with a unique database column behind it, so it can wait in the queue safely. Opening and closing a shift is also online only, because neither operation carries a replay key and a retry could open a second shift.
What is on the screen after a cold start with no connection?
The menu, and not much else. Persistence is decided per endpoint, and the rule is that reference data may be kept on the device while live state may not — so the catalog, the guest menu and the station list survive a relaunch, and open tables, checks, balances and ticket boards do not. A kept copy expires after seven days, is skipped entirely if it would be larger than a megabyte, and is labelled as a saved copy so the screen can say so.
Ask about the case that worries you.
The router that reboots at eight, the basement with one bar of signal, the tablet that ran out of battery mid-order. Those are the questions this design was written against.
A time, a screen share, and your own floor plan on the screen.
[email protected]