What changes when eight people edit the same business records at once
Concurrency and auditability in a small-business operating system: optimistic versus pessimistic updates, where last-write-wins is acceptable and where it is not, idempotency for flaky counter connections, and what offline really promises.
Concurrency is usually discussed as a scaling problem — a database under load, a service with many instances, a queue backing up. For a business with eight people, two counters, and a warehouse phone, it is a different problem with the same mechanics: two people believe they are looking at the same record, they both act, and only one of them is stored.
What follows is how we think about that: which update strategy fits which record, why last-write-wins is fine in some places and unacceptable in others, why every write from a flaky counter needs an idempotency key, where the offline queue boundary actually sits, and what we would have to instrument before claiming any of it works.

Optimistic and pessimistic, and what each one costs
Pessimistic concurrency takes a lock before reading: the second person waits, or is told the record is busy. It is simple to reason about and correct by construction, and it is a poor fit for a counter phone on shop-floor connectivity, because locks have to be held across a network round trip and released reliably, and a process that dies mid-transaction leaves a decision to make about the lock. Optimistic concurrency reads freely, then includes the version it read in the write; if someone else wrote in between, the write is refused and the reader is told to look again.
Optimistic is the better default for our shape of business: reads are frequent, conflicts are rare, and the cost of a conflict is a visible message to one person rather than a stalled till. The cost it imposes is honesty — the interface has to handle 'someone else changed this', and that is a design task, not a detail. Isolation-level documentation, if you want the full mechanics, is well covered in the PostgreSQL documentation; we cite it rather than paraphrasing it badly.
Optimistic vs pessimistic (structural trade-off, not a measured result)
| Dimension | Optimistic (check on write) | Pessimistic (lock on read) |
|---|---|---|
| Read latency | Unaffected | Waits when the record is held |
| Conflict outcome | Write is refused; user re-reads | Second actor waits, then proceeds |
| Network failure mode | Conflict detected at commit | Lock may be left dangling |
| Good fit | Rare conflicts, interactive edits | Short, critical, indivisible operations |
| Cost when wrong | A refused edit, user retries | A blocked counter and a stuck lock |
Where last-write-wins is fine, and where it is not
This is the part most concurrency advice gets wrong by being uniform. A shared record does not need conflict detection just because two people can see it. What matters is the consequence of losing a write, and the consequence is a property of the field, not of the record.
Consequence of a lost write, by field (design position, not measured behaviour)
| Field or operation | Acceptable outcome if writes collide | Why |
|---|---|---|
| Internal note, follow-up date, contact preference | Last write wins | The value is a statement of current intent; the newest is the truest |
| Pipeline stage or task status | Usually last write wins, with the transition recorded | History is what matters, and the transition is an event, not a field |
| Customer-facing invoice number or issued document | Neither — must never be edited in place | It is a document other people may have filed |
| Price or discount | Refuse the stale write and show the current value | A silently overwritten margin decision is unrecoverable and invisible |
| Payment allocation or credit balance | Serialise the write | Two allocations against the same balance must not both be accepted |
| Stock decrement on sale | Atomic decrement against the current quantity | Two counters selling the last unit must not both succeed |
| Employee, manager, or owner capability | Refuse and require an explicit new grant | Silent permission drift is the failure this whole model exists to prevent |
Idempotency: the counter's unsung requirement
A phone on a shop floor loses connectivity in a way a data centre does not. The request is sent, the response is lost, and the honest thing for staff to do is press the button again. If the server is not idempotent, that retry creates a second payment receipt, a second invoice, or a second stock decrement. The user did nothing wrong; the protocol did.
The fix is a client-generated key carried on the write, unique per logical operation, with the server recording the result against it. A repeat submission with a key it has already seen returns the original result instead of performing the work again. This is the same idea HTTP itself applies to idempotent methods, and it is why the pattern is well documented rather than a novelty. Two practical consequences for a small business: the key has to be generated on the device, because the device is the only party that knows the request is a retry; and the original result has to be returned rather than discarded, so the counter sees a success screen and not an error.
What 'offline' actually means, and where the queue boundary sits
Offline is not a mode; it is a set of specific promises, and the useful question is which operations keep them. A device with no connectivity can still do work locally, and that work has to go somewhere later. The honest descriptions break down like this.
What the offline claim actually commits to
Reads of already-synced data
Stale but usable. The user sees the last known state, and the interface has to say how old it is. Without an age indicator, stale data is worse than an error because it looks current.
Writes queued locally
The work is not lost and not yet shared. This is the promise that matters, and it only holds if the queue survives an app restart and if replay order is defined.
Reads that must be current
A live stock figure, a duplicate-check, a revocable permission. These cannot honestly be answered from a stale cache, and the honest answer is 'cannot verify right now' rather than a guess.
The boundary itself
The queue sits between the device and the server, so every queued write is a write that must be reconciled. Queued work that nobody checks after a device has been out of range for a week is not resilient, it is postponed.
What we would need to instrument to know whether we have a problem
Without these, any claim about concurrency is opinion
- Conflict rate per record type — a version-write refusal, counted and reported, not swallowed
- Where conflicts concentrate: which branch, which shift, which field
- Retry volume after a refused write, and how often a user retries into a second conflict
- Duplicate-key hits per day, which indicate retries that reached the server
- Queue depth per device and time-to-drain after reconnect
- Replay outcomes: applied, refused, and left unresolved with a named owner
- Silent-failure count — writes the user believed succeeded and the server rejected
- Cross-device interleavings for the fields we decided must serialise
What we have not measured
To be explicit: we have no measured conflict rate, no measured duplicate-write rate, no measured queue drain time, and no measured availability for a counter on a bad connection. This article is an argument about structure and a list of what should be measured, and we are not presenting it as evidence that any of it performs well. Offline queueing and sync replay for scan validation is listed as a shipped capability in our internal inventory; that is a statement about existence, not about conflict behaviour under load, and the two should not be conflated.
Frequently asked questions
What happens if two people edit the same customer record at once?
It depends on the field, and that is the point. A note or a follow-up date tolerates the newest write winning. A price, a discount, a capability, or a payment allocation should refuse the stale write instead, so the person is shown the current value rather than silently overwriting it.
Do I need a lock so two people cannot edit one record?
Not for most records. Locks cost read latency, need reliable release, and behave badly when a connection drops mid-transaction. Refusing a stale write at commit time gives the same protection for the cases that matter, and turns a conflict into one visible message instead of a blocked till.
Why does a counter payment need an idempotency key?
Because a dropped connection makes a retry the correct user behaviour, not a mistake. If the server treats the retry as a new operation, the customer gets two receipts for one payment. A client-generated key lets the server recognise the repeat and return the original result.
Does the software work without internet?
Partly, and the distinction matters. Reading already-synced data and queueing new work locally are both reasonable offline promises. Anything that needs a current answer — a live stock count, a duplicate check, a permission that may have been revoked — cannot be answered honestly from a stale cache and should say so rather than guess.
How do I know whether I actually have a concurrency problem?
You need instrumentation you cannot get by asking the vendor how fast it is: conflict rate per record type, duplicate-key hits, retry volume after a refused write, queue depth and drain time, and the count of writes the user believed succeeded and the server rejected. Without those, a claim that concurrency is handled is a claim about design, not behaviour.
Is queued offline work really safe from being lost?
Not automatically. A local queue has to survive an app restart, replay in a defined order, and be reconciled by a named person after a device has been out of range. Unreconciled queued work is not resilience; it is a problem that has been postponed to a quieter week.