flowable/flowable-engine · error · ActivitiOptimisticLockingException

<updatedObject> was updated by another transaction concurren

Error message

<updatedObject> was updated by another transaction concurrently

What it means

After executing the update statement, flushUpdates() requires exactly one affected row (updatedRecords != 1 throws). If the UPDATE matched zero rows — typically because another transaction changed the entity's REV_ revision column, so the WHERE REV_ = #{revision} predicate no longer matches — it throws ActivitiOptimisticLockingException. It protects against lost updates between concurrent transactions.

Source

Thrown at modules/flowable5-engine/src/main/java/org/activiti/engine/impl/db/DbSqlSession.java:831

                    revisionEntity.setRevision(revisionEntity.getRevisionNext());
                }
            }
        }
    }

    protected void flushUpdates(List<PersistentObject> updatedObjects) {
        for (PersistentObject updatedObject : updatedObjects) {
            String updateStatement = dbSqlSessionFactory.getUpdateStatement(updatedObject);
            updateStatement = dbSqlSessionFactory.mapStatement(updateStatement);

            if (updateStatement == null) {
                throw new ActivitiException("no update statement for " + updatedObject.getClass() + " in the ibatis mapping files");
            }

            LOGGER.debug("updating: {}", updatedObject);
            int updatedRecords = sqlSession.update(updateStatement, updatedObject);
            if (updatedRecords != 1) {
                throw new ActivitiOptimisticLockingException(updatedObject + " was updated by another transaction concurrently");
            }

            // See https://activiti.atlassian.net/browse/ACT-1290
            if (updatedObject instanceof HasRevision) {
                ((HasRevision) updatedObject).setRevision(((HasRevision) updatedObject).getRevisionNext());
            }

        }
        updatedObjects.clear();
    }

    protected void flushDeletes(List<DeleteOperation> removedOperations) {
        boolean dispatchEvent = false;
        FlowableEventDispatcher eventDispatcher = Context.getProcessEngineConfiguration().getEventDispatcher();
        if (eventDispatcher != null && eventDispatcher.isEnabled()) {
            dispatchEvent = eventDispatcher.isEnabled();
        }

View on GitHub (pinned to d6d39ce1c6)

Solutions

  1. Catch ActivitiOptimisticLockingException and retry the command (or use the engine's retry mechanisms, e.g. async job retries) after re-reading fresh state.
  2. Enable exclusive jobs / proper locking for the async executor so the same execution isn't processed on two nodes simultaneously.
  3. Shorten transactions and re-fetch entities right before modification to reduce stale-revision windows.
  4. If updates are unconditional by design, adjust the mapping to not predicate on REV_ and remove revision increments — only with a clear understanding of lost-update risk.

Example fix

// before: unconditional second write
execution.setName("b"); // execution was concurrently updated elsewhere
context.getDbSqlSession().flushUpdates();

// after: retry on optimistic lock
try {
  execution.setName("b");
  flushUpdates();
} catch (ActivitiOptimisticLockingException e) {
  Execution fresh = runtimeService.createExecutionQuery().executionId(id).singleResult();
  fresh.setName("b");
  save(fresh);
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Optionally verify the revision hasn't moved before updating
HasRevision e = entity;
if (revisionInDb(e.getId()) != e.getRevision()) {
    reload(entity); // refresh snapshot before mutating
}

Try / catch

try {
    entity.setName("new");
    save(entity);
} catch (ActivitiOptimisticLockingException e) {
    // someone else updated first: re-read, re-apply, retry
    Object fresh = reloadAndReapply(entity.getId());
    save(fresh);
}

Prevention

When it happens

Trigger: Two transactions load the same revisioned entity; both flush updates; the second one's UPDATE matches 0 rows because the first already incremented REV_, triggering updatedRecords != 1.

Common situations: Concurrent task/execution mutations from clustered async executors; the same job acquired by two nodes (missing exclusive-job config); user actions and timers racing on the same process instance; long transactions holding stale snapshots.

Related errors


AI-assisted analysis of flowable/flowable-engine@d6d39ce1c6 (2026-09-11). Data as JSON: /api/errors/b9e1935f08e2e156. Report an issue: GitHub.