Operations

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.

ConcurrencyData IntegrityOfflineIdempotencyNoxOrigin

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.

Diagram: two people editing one shared record at the same moment, where changes in the upper part of the record merge cleanly and changes in the lower part collide and are diverted into a queue.
The same record, two people, one moment. The top region tolerates a lost write. The bottom region has to be queued and resolved, because a lost write there is money.

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)

DimensionOptimistic (check on write)Pessimistic (lock on read)
Read latencyUnaffectedWaits when the record is held
Conflict outcomeWrite is refused; user re-readsSecond actor waits, then proceeds
Network failure modeConflict detected at commitLock may be left dangling
Good fitRare conflicts, interactive editsShort, critical, indivisible operations
Cost when wrongA refused edit, user retriesA 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 operationAcceptable outcome if writes collideWhy
Internal note, follow-up date, contact preferenceLast write winsThe value is a statement of current intent; the newest is the truest
Pipeline stage or task statusUsually last write wins, with the transition recordedHistory is what matters, and the transition is an event, not a field
Customer-facing invoice number or issued documentNeither — must never be edited in placeIt is a document other people may have filed
Price or discountRefuse the stale write and show the current valueA silently overwritten margin decision is unrecoverable and invisible
Payment allocation or credit balanceSerialise the writeTwo allocations against the same balance must not both be accepted
Stock decrement on saleAtomic decrement against the current quantityTwo counters selling the last unit must not both succeed
Employee, manager, or owner capabilityRefuse and require an explicit new grantSilent 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.

Sources and further reading

Continue reading

Looking for the rest of this topic? More in business operations →

BillingWhy a ₹15,000 invoice and a ₹15,000 payment are not the same recordRead guide →TicketsHow QR Ticket Validation Works: Valid, Duplicate, Expired and Invalid TicketsRead guide →commerceThe Stock Count That Disagrees With the LedgerRead guide →