Lost Update

A lost update occurs when concurrent transactions read the same older value and later write conflicting changes, causing one valid change to overwrite another. It is a data-integrity problem commonly addressed with optimistic locking, pessimistic locking, atomic database operations, or appropriate transaction isolation.

The classic pattern is read-modify-write: transaction A reads a row, transaction B reads the same row, both calculate new values independently, and their writes occur without coordinating with each other. The later write can replace the earlier result even though both transactions completed successfully.

For example, suppose a balance is `100`. A and B both read `100`; A computes `110`, while B computes `80`. If A writes `110` and B subsequently writes `80`, the database ends at `80`, losing A's valid update. The application may see no exception because each individual write was valid.

In Java backends, the safest solution depends on the operation and concurrency requirements. Optimistic locking commonly adds a version column and updates only when the version still matches the value that was read. Pessimistic locking can serialize access by locking the relevant database row, while atomic SQL such as `UPDATE account SET balance = balance + ?` can avoid a separate read-modify-write sequence.

Transaction isolation alone should not be treated as a universal fix. The exact behavior depends on the database engine, isolation level, SQL statements, and locking semantics. A robust design makes the concurrency assumption explicit and checks whether an attempted update actually affected the expected row.

Key points

Interview questions

What is a lost update?

A lost update happens when two concurrent transactions modify the same data based on the same earlier value. One transaction writes its result, and the other later writes a result based on the stale value. The later write can therefore erase the earlier change. Both operations may appear successful from the application's perspective.

How would you prevent a lost update in a Java backend?

One common approach is optimistic locking with a version column. The update includes the version that was originally read, such as `WHERE id = ? AND version = ?`, and increments the version when the update succeeds. If zero rows are affected, another transaction changed the record first, so the application can retry, reject the operation, or report a conflict. For operations that can be expressed arithmetically, an atomic SQL update can sometimes be simpler.

Why can `SELECT` followed by `UPDATE` cause a lost update?

The `SELECT` gives each transaction a snapshot of the value it is using for its calculation. If another transaction changes the row before the first transaction performs its `UPDATE`, the first transaction may still write a result derived from the old value. Without a condition that detects the change or a lock that prevents it, the second write can overwrite the first transaction's result. Combining the operation into an atomic database expression can avoid this particular race.

Does Java `synchronized` prevent lost updates in a database?

Not generally. `synchronized` coordinates threads that share the same Java object within the same JVM, but it does not automatically coordinate other application instances or transactions accessing the same database. A multi-instance service therefore needs database-level concurrency control or another distributed coordination mechanism. Even in one JVM, the synchronization boundary must cover the complete read-modify-write operation to help.

How would you design optimistic locking for a JPA entity?

A typical design adds a version field to the entity and uses JPA's optimistic-locking support so an update checks that the version has not changed since it was read. If another transaction has already modified the entity, the stale transaction receives an optimistic-lock failure rather than silently overwriting the newer state. The service must then decide whether to retry, merge changes, or return a conflict to the caller. Retry policies should be bounded because repeated contention can otherwise amplify load.