Concurrency and Simultaneous Editing in Business Apps: Stopping Users Overwriting Each Other
A technical guide for teams whose users keep losing each other's work: row version checks, expiring locks, field-level saves and append-only records, on Dataverse and on a PostgreSQL backend, plus a conflict screen that never loses typing.
When users edit the same records and work vanishes, the app is saving whole records with last-write-wins. Four mechanisms fix it. Optimistic concurrency: every save carries the row version it was based on, and a stale save is rejected instead of silently winning, using If-Match with the row etag on the Dataverse Web API or UPDATE ... WHERE version matches in PostgreSQL. Advisory locks with an expiry, so an abandoned lock never blocks anyone for long. Field-level saves that send only changed columns, so edits to different fields both survive. Append-only records for notes, readings and lines two people genuinely add at once. When a conflict happens, keep the user's typing and show both values side by side.
Why do users overwrite each other in the same record?
Because the default behaviour of most business apps is last-write-wins. Two people open the same record, both change it, and whoever saves second replaces what the first person saved without either of them being told. Nobody sees an error, so the problem is reported as "the app loses my work" weeks later.
Three design choices make it worse. Saving the whole record rather than the columns the user changed, so an untouched field on a stale screen overwrites a fresh value. Long-lived screens, where a record is loaded in the morning and saved after lunch. And single wide records holding things several people add to, such as one notes field or one total, so every contributor competes for the same column.
Lost work destroys adoption faster than almost any other defect, because users stop trusting the system and keep their own spreadsheet as a backup. The fixes below are senior engineering decisions about the data model and the save path, which is why they rarely appear in apps built quickly by citizen developers.
- Two users edit the same column: someone must lose, and the question is whether they are told.
- Two users edit different columns: nobody needs to lose, if only changed columns are saved.
- Two users add information to the same thing: nobody needs to lose, if each contribution is its own row.
- One user holds a record open for a long task: a lock with an expiry may be kinder than a conflict at the end.
How does optimistic concurrency with a row version stop a stale save?
Every row carries a version that changes on every update. The client remembers the version it loaded, and the save succeeds only if the row still has that version. If someone else saved in between, the save is refused and the app can tell the user, rather than overwriting. It is called optimistic because nothing is locked while people edit: conflicts are assumed to be rare and are detected at save time.
The important part is where the check happens. It must be in the same operation as the write, on the server or in the database. A client that reads the record, compares, and then writes has a gap in which another save can slip through.
| Step | Dataverse | PostgreSQL backend |
|---|---|---|
| Version carried by the row | The versionnumber column, exposed in Web API responses as @odata.etag | A version integer column incremented on every update |
| Client keeps the loaded version | Store the @odata.etag returned by the GET | Return the version with the record and send it back on save |
| Conditional write | PATCH with the If-Match header set to the stored etag | UPDATE ... WHERE id = $1 AND version = $2 |
| Conflict signal | HTTP 412 Precondition Failed | Zero rows updated, returned to the client as a conflict response |
How do you implement optimistic concurrency on Dataverse?
It depends on which client writes the data, because each one behaves differently. Check current Microsoft documentation for which tables have optimistic concurrency enabled: the table metadata property IsOptimisticConcurrencyEnabled tells you, and custom tables support it.
| Client | Default behaviour | How to detect conflicts |
|---|---|---|
| Dataverse Web API (integrations, PCF controls, custom pages, portals code) | An update without a condition simply applies | Send If-Match with the @odata.etag from the read; a 412 Precondition Failed response means the row changed since it was loaded |
| Dataverse SDK for .NET (plug-ins, services, integrations) | UpdateRequest applies regardless of version | Set RowVersion on the entity and ConcurrencyBehavior to IfRowVersionMatches on the UpdateRequest; a version mismatch raises a fault you catch and handle |
| Model-driven forms | Only changed columns are sent on save, so edits to different columns both survive; the same column edited twice is last save wins | Add an expected version check enforced by a synchronous plug-in, described below |
| Canvas apps with Patch or SubmitForm | Patch sends the columns you pass and has no built-in etag check; a form submits the values of its cards | Patch only changed columns, and route saves that must not collide through a server-side version check |
| Power Automate Dataverse actions | Update a row applies the values it is given | Keep flows off columns users edit, or call a custom API that performs a version-checked update |
How do you stop canvas apps and model-driven forms silently overwriting a newer save?
Use one server-side pattern that every client shares, so the rule cannot be bypassed. Add a whole number column such as "Edit version" to the table. A synchronous pre-operation plug-in on update increments it on every save, and when the incoming update carries an expected version that differs from the current value, the plug-in cancels the save with a clear message. Because it runs inside the save transaction, there is no gap between the check and the write.
In a canvas app, load the edit version with the record, keep it in a variable, and include it in the Patch together with only the columns the user changed. If the Patch fails with the conflict message, handle it in the app rather than letting the default error banner appear. In a model-driven form, put the edit version column on the form hidden, and use a small form script to set its submit mode to always, because forms otherwise send only columns the user changed and the plug-in would never see the expected version. When the plug-in refuses the save, the form keeps the user's unsaved values on screen.
A client-only check, comparing Modified On before calling Patch, narrows the window but does not close it, so reserve it for low-risk screens. Enforcing rules server side for canvas apps in general is covered in our guide to Dataverse business rules and canvas apps. Test the plug-in against integrations and flows too, and decide whether system processes should be exempt.
How do you implement a row version check on a PostgreSQL backend?
On a custom backend the database gives you the whole mechanism in one statement. Add a version column (integer, not null, default 1) to each table users edit together. The API returns the version with every record, and the save runs UPDATE with SET for the changed columns and version = version + 1, WHERE id = $1 AND version = $2, RETURNING the new row. One row returned means the save succeeded; zero rows means either the record no longer exists or someone else saved first, and a follow-up read tells you which.
Expose it over HTTP in the standard way: return the version as an ETag header, require If-Match on updates, and answer 412 Precondition Failed or 409 Conflict with the current record in the body so the client can show both versions without another round trip. Put the check in the data access layer so no endpoint can forget it, and write a test that performs two saves from the same starting version and expects the second to be rejected. This is how we build the save path in a custom CRM on Node.js or .NET with PostgreSQL.
When should you use an advisory lock with an expiry instead?
When the editing task is long and a conflict at the end would waste real effort: a complex quote, a case being rewritten, a planning record a coordinator works on for half an hour. A lock tells the second person up front that someone else is working on it. It is advisory, which means the app shows it and respects it, and it must expire, because users close laptops, lose signal and go home without releasing anything. Keep optimistic concurrency underneath, because a lock that has expired or been taken over must not let a stale save through.
- PostgreSQL: a record_locks table with record type, record id, locked by, acquired at and expires at, and a primary key on record type and record id. Acquire with INSERT ... ON CONFLICT DO UPDATE only WHERE the existing lock has expired or belongs to the same user; zero rows affected means someone else holds it.
- Do not use PostgreSQL session advisory locks for this. They belong to a database connection, and with connection pooling and stateless web requests they do not map to a person holding a record for minutes.
- Dataverse: there is no built-in editing lock for ordinary rows, so use a lock table or "Locked by" and "Lock expires" columns, set through a custom API or synchronous plug-in that grants the lock only when it is free or expired.
- Renew the lock with a heartbeat while the screen is open, and release it on save or when the user leaves; the expiry covers every case where release never happens.
- Show who holds the lock and until when, offer read-only viewing, and let a supervisor break a lock deliberately, with the break recorded.
- Keep expiry short, typically minutes renewed by heartbeat rather than hours, so a crashed session frees the record quickly.
How do field-level saves reduce conflicts?
Most collisions are two people changing different things on the same record. If the save sends only the columns each user changed, both edits survive and there is nothing to resolve. Model-driven forms and Dataverse Web API PATCH requests already work this way; canvas apps do when Patch is given only the changed columns rather than a whole record copied from a gallery; and a custom API should accept partial updates and build the SET clause from the columns actually submitted.
Where the data model allows, go further and split records along ownership lines. Contact details owned by the office, a status owned by the person doing the work and approval columns owned by a manager can live on the same row if each role can only edit its own columns, or on separate related rows if they change at different rhythms. Field-level saves do not help when two people change the same column; that still needs a version check or a different data shape.
When do append-only records beat editing a shared record?
When two people genuinely must contribute to the same thing at the same time. Notes, readings, time entries, comments, allocations and quantities are not one value to fight over; they are a list of contributions. Store each contribution as its own inserted row with author and time, never update it, and derive the current total or latest value from the rows. Two simultaneous inserts cannot overwrite each other, and the history is kept for free.
On Dataverse, that means a child table where security roles grant create and read but not update or delete, a rollup or calculated summary for the current figure, and optionally a plug-in that blocks edits. The full design, including protecting the rows and backfilling from audit history, is on our append-only ledger guide. On PostgreSQL, an insert-only events table with a projection or view for the current state, and database permissions that deny UPDATE and DELETE to the application role. The same principle is what makes offline field capture safe, covered in our guide to offline field apps that do not lose data.
How do you show a conflict to a non-technical user without losing their typing?
Treat the user's unsaved input as the most valuable data on the screen. A conflict message that says "record changed, please reload" and then throws away twenty minutes of typing is worse than the silent overwrite it replaced, and users learn to reload first and paste later.
- Keep the draft: hold the user's changes in local state, and in a canvas app a collection saved with SaveData, before attempting the save, so nothing is lost if the save fails.
- Fetch the current record and compare column by column: columns only the user changed can be applied on top of the fresh version automatically.
- For columns both people changed, show "Your change", "Saved by someone else" with their name and time, and a choice per column, in plain language rather than error codes.
- Resave with the new version, so the resolved save is itself protected.
- For long text, offer to keep both, for example by adding the user's text as a new note rather than choosing one version.
- Log conflicts, including table, columns and frequency, so you can see where the data model needs a split or an append-only table instead of more dialogs.
Does fixing concurrency change whether the app belongs on Power Platform or a custom build?
Usually not. Dataverse supports row version checks, partial updates, plug-ins for server-side enforcement and child tables for append-only data, so a Power Platform app can handle fifty or more concurrent editors properly once the save path is designed. What a Power Platform engagement with senior developers covers is on our Power Platform consulting page.
We recommend the right solution - whether that's Microsoft Dynamics 365, Power Platform, or a custom-built CRM. Some businesses need the Microsoft ecosystem. Others need full control without licensing. We deliver both. When an application is mostly custom logic, needs full control over the save path and hosting, or must avoid per-user licensing for a large editing population, a custom-built CRM on React, Node.js, PostgreSQL or .NET gives you the database-level mechanisms above directly.
Should an app with many concurrent editors run on Power Platform or a custom build?
Can afford licensing and want the Microsoft ecosystem
Dynamics 365
Microsoft 365, Teams and Outlook integration, a mature partner ecosystem, Copilot, and apps for sales, service and field operations that are configured rather than built.
Need full control and zero licensing
Custom CRM
A CRM built on React, Node.js, PostgreSQL or .NET that you own outright: your data model, your hosting, no per-user subscription, and features shaped exactly to your process.
Not sure which fits
We help you decide
A short discovery weighs licensing budget, process complexity, integrations and long-term ownership, then recommends one path. We deliver both, so the recommendation has no reason to lean.
How does Solzet fix an app where users lose each other's work?
We start by finding where the overwrites happen: audit history or database logs for the affected tables, the clients that write to them, and which columns collide. Then we choose the lightest mechanism per case: partial saves where people edit different columns, a version check where they edit the same ones, a lock with expiry for long tasks, and append-only tables where they add information together, plus a conflict screen that keeps the user's typing. Auditing configuration to support that investigation is covered in our Dynamics 365 audit trail guide.
The work is done by senior consultants and full-stack developers delivering remotely from Yerevan, Armenia, with 8+ years of Dynamics 365 Customer Engagement, Power Platform and custom development, directly for your team or white-label for Microsoft partners.
What do people ask us?
Why does my business app let users overwrite each other's changes?
Because saves are last-write-wins by default. Two people load the same record, both change it, and the second save replaces the first without anyone being told. Saving whole records instead of changed columns, keeping screens open for hours and storing shared information in single fields make it far more frequent.
How does Dataverse optimistic concurrency work?
Each row has a versionnumber, exposed by the Web API as @odata.etag. Send an update with the If-Match header set to the etag you read, and Dataverse rejects it with 412 Precondition Failed if the row has changed since. In the .NET SDK, set RowVersion and ConcurrencyBehavior.IfRowVersionMatches on the UpdateRequest. Check current documentation for which tables support it.
Do model-driven forms in Dynamics 365 overwrite other users' changes?
Only when both users change the same column. Model-driven forms save the columns the user changed, so edits to different columns both survive. For the same column the later save wins without a warning, so add an expected version column that a synchronous plug-in checks if those collisions matter.
Does Patch in a canvas app check for conflicts?
No. Patch writes the columns you pass and has no built-in etag or version check. Patch only the columns the user changed, and for records where collisions matter, include an expected version column that a synchronous plug-in compares with the current value, cancelling the save when they differ.
How do you detect a conflicting update in PostgreSQL?
Add a version column and update with SET version = version + 1 WHERE id = $1 AND version = $2. If the statement updates zero rows, someone else saved first or the row was deleted, so return a conflict with the current record. Expose the version as an ETag and require If-Match on the API.
Should we lock records while a user is editing?
Only for long editing tasks where a conflict at the end would waste real work, and only with an expiry renewed by a heartbeat, so an abandoned session never blocks others for long. Show who holds the lock, let a supervisor break it, and keep a version check underneath for saves after a lock expires.
What is the best way to let two people edit the same thing at once?
Stop storing it as one value. Notes, readings, time, comments and quantities become separate inserted rows with author and time, never updated, and the current figure is derived from them. Simultaneous inserts cannot overwrite each other, and you keep a full history of who contributed what.
How should an app tell a user about an editing conflict?
Without losing their input. Keep the draft locally, reload the current record, apply non-conflicting columns automatically, and show the conflicting ones side by side as "your change" and "saved by someone else" with the name and time, letting the user choose per column before saving again with the new version.
Where should you go next?
Power Platform consulting
Senior developers for Power Apps, Dataverse, plug-ins, custom APIs and PCF controls.
Custom CRM Development
Applications on React, Node.js, PostgreSQL and .NET with the save path designed in, without Microsoft licensing.
Offline field apps that do not lose data
Sync conflicts, last-write-wins per field and reconciling offline edits.
Append-only ledger in Dataverse
Insert-only rows protected by security roles and plug-ins, with history kept.
Dataverse business rules and canvas apps
Enforcing rules on the server so every client, including canvas apps, respects them.
Dynamics 365 audit trail
Auditing configuration to trace who changed what and when.
Which solution is right for your business?
Tell us what you need. A senior consultant replies within one business day with a recommendation - Dynamics 365, Power Platform, or a custom-built CRM - not a sales script.