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
- Catch ActivitiOptimisticLockingException and retry the command (or use the engine's retry mechanisms, e.g. async job retries) after re-reading fresh state.
- Enable exclusive jobs / proper locking for the async executor so the same execution isn't processed on two nodes simultaneously.
- Shorten transactions and re-fetch entities right before modification to reduce stale-revision windows.
- 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
- Keep transactions short; don't hold entity snapshots across long operations.
- Use exclusive jobs to serialize work on the same process instance/execution.
- Treat ActivitiOptimisticLockingException as transient and build retry into command handlers.
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
- ${updatedObject} was updated by another transaction concurre
- ${entity} was updated by another transaction concurrently
- <persistentObject> was updated by another transaction concur
- One of the entities <persistentObjectClass> was updated by a
- Could not lock case instance
AI-assisted analysis of flowable/flowable-engine@d6d39ce1c6 (2026-09-11).
Data as JSON: /api/errors/b9e1935f08e2e156.
Report an issue: GitHub.