flowable/flowable-engine · error · FlowableOptimisticLockingException

${updatedObject} was updated by another transaction concurre

Error message

${updatedObject} was updated by another transaction concurrently

What it means

FlowableOptimisticLockingException thrown when the UPDATE affects zero rows, meaning the row's revision in the DB no longer matches the in-memory entity's revision — another transaction modified the row first.

Source

Thrown at modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/db/DbSqlSession.java:622

        }

        updatedObjects.clear();
        bulkUpdateOperations.clear();
    }

    protected void flushUpdateEntity(Entity updatedObject) {
        String updateStatement = dbSqlSessionFactory.getUpdateStatement(updatedObject);
        updateStatement = dbSqlSessionFactory.mapStatement(updateStatement);

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

        LOGGER.debug("updating: {}", updatedObject);

        int updatedRecords = sqlSession.update(updateStatement, updatedObject);
        if (updatedRecords == 0) {
            throw new FlowableOptimisticLockingException(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());
        }
    }

    protected void flushBulkUpdate(BulkUpdateOperation bulkUpdateOperation) {
        // Bulk update
        bulkUpdateOperation.execute(sqlSession);
    }

    protected void flushDeletes() {

        if (deletedObjects.size() == 0 && bulkDeleteOperations.size() == 0) {
            return;
        }

View on GitHub (pinned to d6d39ce1c6)

Solutions

  1. Retry the whole command (OptimisticLockingAwareTransactionInterceptor retries FlowableOptimisticLockingException for async jobs) so the entity is reloaded with a fresh revision.
  2. Serialize access: use exclusive jobs or acquire a DB lock before mutating shared entities.
  3. Shorten transaction scope so stale reads are less likely.
  4. For async executors, increase retry settings and ensure the job handler is idempotent.

Example fix

// before
managementService.executeCommand(ctx -> { task.setName(name); return null; }); // may throw FlowableOptimisticLockingException
// after
for (int i = 0; i < 3; i++) {
  try { managementService.executeCommand(ctx -> { task.setName(name); return null; }); break; }
  catch (FlowableOptimisticLockingException e) { task = taskService.createTaskQuery().taskId(task.getId()).singleResult(); }
}
Defensive patterns

Strategy: validation

Validate before calling

String stmt = dbSqlSessionFactory.getUpdateStatement(entity);
if (dbSqlSessionFactory.mapStatement(stmt) == null) {
  throw new IllegalStateException("No update statement mapped for " + entity.getClass());
}

Try / catch

try { /* persist entity */ }
catch (FlowableException e) { if (e.getMessage().startsWith("no update statement")) { /* add mapping and retry after deploy */ } throw e; }

Prevention

When it happens

Trigger: Two concurrent commands/transactions load the same entity, both modify it; the second flush executes its versioned UPDATE which matches 0 rows because REV_ was already incremented.

Common situations: Long-running user tasks edited by multiple users; async jobs and a human action touching the same process instance; retrying a stale command; clustered engines sharing one DB.

Related errors


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