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

  1. Catch FlowableOptimisticLockingException and treat the delete as already done, or reload the entity and retry the command.
  2. Check first (query) whether the entity still exists before deleting in interactive flows.
  3. Use exclusive jobs/locking for async work touching the same rows.
  4. 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

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


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