flowable/flowable-engine · error · FlowableOptimisticLockingException
${entity} was updated by another transaction concurrently
Error message
${entity} was updated by another transaction concurrently What it means
FlowableOptimisticLockingException thrown when a versioned DELETE removes zero rows: the row was already modified (revision bumped) or deleted by another transaction, so the revision-guarded delete matched nothing.
Source
Thrown at modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/db/DbSqlSession.java:690
bulkDeleteOperation.execute(sqlSession, entityClass);
}
}
}
protected void flushDeleteEntities(Class<? extends Entity> entityClass, Collection<Entity> entitiesToDelete) {
for (Entity entity : entitiesToDelete) {
String deleteStatement = dbSqlSessionFactory.getDeleteStatement(entity.getClass());
deleteStatement = dbSqlSessionFactory.mapStatement(deleteStatement);
if (deleteStatement == null) {
throw new FlowableException("no delete statement for " + entity.getClass() + " in the ibatis mapping files");
}
// It only makes sense to check for optimistic locking exceptions
// for objects that actually have a revision
if (entity instanceof HasRevision) {
int nrOfRowsDeleted = sqlSession.delete(deleteStatement, entity);
if (nrOfRowsDeleted == 0) {
throw new FlowableOptimisticLockingException(entity + " was updated by another transaction concurrently");
}
} else {
sqlSession.delete(deleteStatement, entity);
}
}
}
@Override
public void close() {
sqlSession.close();
}
public void commit() {
sqlSession.commit();
}
public void rollback() {
sqlSession.rollback();View on GitHub (pinned to d6d39ce1c6)
Solutions
- Catch FlowableOptimisticLockingException and treat the delete as already done, or reload the entity and retry the command.
- Check first (query) whether the entity still exists before deleting in interactive flows.
- Use exclusive jobs/locking for async work touching the same rows.
- Keep transactions short to reduce stale-revision windows.
Example fix
// before
taskService.deleteTask(taskId); // races with concurrent modification -> exception
// after
try { taskService.deleteTask(taskId); }
catch (FlowableOptimisticLockingException e) { /* already deleted/changed; verify and continue */ } Defensive patterns
Strategy: validation
Validate before calling
String stmt = dbSqlSessionFactory.getDeleteStatement(MyEntityImpl.class);
if (dbSqlSessionFactory.mapStatement(stmt) == null) {
throw new IllegalStateException("No delete statement mapped for " + MyEntityImpl.class);
} Try / catch
try { /* delete entity */ }
catch (FlowableException e) { if (e.getMessage().startsWith("no delete statement")) { /* add mapping and redeploy */ } throw e; } Prevention
- Test CRUD (insert/update/delete) for every custom entity in an in-memory DB test.
- Register all three statement names in DbSqlSessionFactory when introducing an entity.
- Verify mapper XMLs are packaged and listed in MyBatis mapper locations.
- After upgrades, diff custom mappers against new built-in ones.
When it happens
Trigger: Concurrent transactions: one deletes or updates a HasRevision entity while another tries to delete it; the second delete executes WHERE REV_ = staleRevision and deletes 0 rows.
Common situations: Two users deleting the same task/history row; async job racing a user-initiated cancel; stale in-memory entity held across transactions; clustered engines on a shared database.
Related errors
- ${updatedObject} was updated by another transaction concurre
- <persistentObject> was updated by another transaction concur
- One of the entities <persistentObjectClass> was updated by a
- <updatedObject> was updated by another transaction concurren
- Could not lock case instance
AI-assisted analysis of flowable/flowable-engine@d6d39ce1c6 (2026-09-11).
Data as JSON: /api/errors/ba02cbf901dea3d0.
Report an issue: GitHub.