From CRUD to live editing: handling concurrent changes

Created: June 2023 Updated: Sept. 29, 2026

from work

Python, Django, PostgreSQL

What to do when several people change the same record at once.

Two people open the same record, each changes something and saves. In an ordinary CRUD the second save overwrites the first and nobody ever finds out. With one user you don't see it, tests are usually done by one person, so it only shows up in production, as data that “changed by itself”.

What you can do about it

The simplest option is nothing, the last save wins. Sometimes that really is enough, e.g. when a record is always edited by one person.

The first thing that comes to mind is locking the record when it's opened, like in a system I worked on years ago: the first person locks it, the second sees who holds it and can ask for it to be released. Nothing gets lost, but locks are left behind by people who walked away with the form open, so they have to be released by hand or after a timeout.

Instead of locking, you can keep a version in the record and check on save that nobody changed it in the meantime. There are no locks, and a conflict pops up only on a real collision.

Or stop editing data directly altogether and submit change proposals that go through approval, like the surveys in the system I'm working on now. Every change has an author and an approver, changes to different parts don't get in each other's way, except a change isn't visible straight away.

And there's live editing, like in Google Docs, where several people write at once and see each other. Underneath there are merge algorithms, OT or CRDT, and ready-made libraries such as Yjs, but that's a whole different story from a form with a Save button.

What I'd pick

Most of the time I'd pick a version in the record, because it's cheap and blocks nobody, and change proposals where changes have to be approved anyway, like our surveys.

A version in the record in Django is one field and a condition on save:

python
from django.db.models import F

from documents.models import Document


class Conflict(Exception):
    pass


def save_document(doc_id: int, version: int, **changes) -> None:
    updated = Document.objects.filter(pk=doc_id, version=version).update(**changes, version=F("version") + 1)
    if not updated:
        raise Conflict("Someone changed this document in the meantime")

If update doesn't change any row, someone was faster, and instead of overwriting their changes we show a conflict.

Machine-translated from Polish (original).