I'm building an invoicing system with Spring Boot and Angular, and I'm trying to handle concurrent edits correctly. The database can safely handle normal cases like one user exporting invoices while another creates a new invoice, but there's a problem when two users open and edit the same invoice at nearly the same time. If both load invoice #12, make different changes, and save, the second save can silently overwrite the first user's work.
I've implemented optimistic locking with a JPA/Hibernate `@Version` column, so a save fails when the database version no longer matches the version the user originally loaded. I've also considered pessimistic locking with `SELECT FOR UPDATE` for situations where the record should be locked during editing.
Optimistic locking seems like the usual choice for CRUD applications, but I'm unsure how to handle the conflict in the user interface. Should the user simply be told to reload? Should the application show the latest version alongside their changes and let them merge the differences? Is it worth displaying an indicator that someone else is currently editing the invoice? What approaches work well in modern web applications?
3 Answers
Optimistic locking is a solid default when conflicts are relatively uncommon. Store the version the client loaded and require it to match the current database version during the update. If it doesn’t match, return a conflict response instead of overwriting anything.
For the UI, reload the latest record, preserve the user’s unsaved values, and show which fields changed so they can choose what to keep. A simple “someone else updated this invoice” message may be enough for small edits, but field-level merging is much friendlier when users have entered a lot of data. Full collaborative-editing algorithms such as operational transformation or CRDTs are usually unnecessary unless users need simultaneous, document-style editing.
A useful UX combination is to check for changes when the user saves and optionally show a lightweight “currently being edited” indicator while the form is open. The indicator is only a hint, though—it should not replace optimistic locking because browsers can close, networks can fail, and users can leave a form open indefinitely. For the actual conflict, show the latest server version and let the user review or merge their changes.
A reload-and-highlight-differences flow is usually better than a generic error toast. It also avoids losing the user’s draft when the conflict is detected.
The standard pattern is called optimistic concurrency control or version-based concurrency control. Add a version number, timestamp, or row-version value, send it with the update, and make the update conditional on that value still being current. If the update affects zero rows, another user changed the record and the API should return a conflict rather than silently replacing it.

So the backend usually just needs version checking and a clear conflict response? The merge or diff experience can be handled by the frontend based on the submitted values and the newly fetched record.