Skip to main content
One idea at a time

A version prevents silent overwrite

Maya and Sam both open a task at version 4. Maya changes the title. Sam changes the status using the stale copy still displayed in his browser.

If both requests replace the row without checking a version, Sam's request can overwrite Maya's change. The UI might show success to both people while losing one person's work.

Taskboard uses a version number to detect stale writes. A task response contains the current version. The client sends that value as expectedVersion when requesting a change.

-- Illustrative conditional update.
UPDATE tasks
SET title = $1, version = version + 1
WHERE workspace_id = $2
AND id = $3
AND version = $4
RETURNING id, title, version;

The version condition and update happen in the same statement. If Maya's update changes version 4 to 5, Sam's update expecting 4 cannot match the row.

A conflict is useful information​

The server reports a conflict instead of pretending Sam's update succeeded. His frontend can refetch the task, show the new value, and let him decide whether to reapply the change.

This differs from automatically retrying an operation after a temporary database failure. Repeating Sam's stale write with a fresh version could overwrite data he has never seen. A retry must preserve the meaning of his original request.

The workspace predicate remains necessary. A version number does not authorize access to the task. The API checks scope before interpreting the write's result.

Locks solve a different shape of problem​

Optimistic concurrency works well when conflicts are occasional and the user can resolve them. A rule involving several records may need a transaction that locks a shared row before checking a limit.

For example, two invitation acceptances must coordinate when only one membership slot remains. A task version cannot protect a workspace-wide member count by itself.

A common mistake is reading the version, comparing it in JavaScript, and then issuing an unconditional update. Another writer can commit between the comparison and update. Put the condition where the write occurs.

Why use an integer version instead of an updated timestamp?

A version gives an explicit increment for each accepted edit. Timestamps have precision and serialization concerns and can coincide, so they require more care as concurrency tokens.

A stale write is a known conflict. Detect it atomically and let the client respond to the newer state.

Read api/src/tasks.ts