{"record":{"id":"97ab22839f905764","repo":"flowable/flowable-engine","slug":"updatedobject-was-updated-by-another-transactio","errorCode":null,"errorMessage":"${updatedObject} was updated by another transaction concurrently","messagePattern":"(.+?) was updated by another transaction concurrently","errorType":"exception","errorClass":"FlowableOptimisticLockingException","httpStatus":null,"severity":"error","filePath":"modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/db/DbSqlSession.java","lineNumber":622,"sourceCode":"        }\n\n        updatedObjects.clear();\n        bulkUpdateOperations.clear();\n    }\n\n    protected void flushUpdateEntity(Entity updatedObject) {\n        String updateStatement = dbSqlSessionFactory.getUpdateStatement(updatedObject);\n        updateStatement = dbSqlSessionFactory.mapStatement(updateStatement);\n\n        if (updateStatement == null) {\n            throw new FlowableException(\"no update statement for \" + updatedObject.getClass() + \" in the ibatis mapping files\");\n        }\n\n        LOGGER.debug(\"updating: {}\", updatedObject);\n\n        int updatedRecords = sqlSession.update(updateStatement, updatedObject);\n        if (updatedRecords == 0) {\n            throw new FlowableOptimisticLockingException(updatedObject + \" was updated by another transaction concurrently\");\n        }\n\n        // See https://activiti.atlassian.net/browse/ACT-1290\n        if (updatedObject instanceof HasRevision) {\n            ((HasRevision) updatedObject).setRevision(((HasRevision) updatedObject).getRevisionNext());\n        }\n    }\n\n    protected void flushBulkUpdate(BulkUpdateOperation bulkUpdateOperation) {\n        // Bulk update\n        bulkUpdateOperation.execute(sqlSession);\n    }\n\n    protected void flushDeletes() {\n\n        if (deletedObjects.size() == 0 && bulkDeleteOperations.size() == 0) {\n            return;\n        }","sourceCodeStart":604,"sourceCodeEnd":640,"githubUrl":"https://github.com/flowable/flowable-engine/blob/d6d39ce1c69ff244f2d9dc6af756a9b95e865586/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/db/DbSqlSession.java#L604-L640","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Retry the whole command (OptimisticLockingAwareTransactionInterceptor retries FlowableOptimisticLockingException for async jobs) so the entity is reloaded with a fresh revision.","Serialize access: use exclusive jobs or acquire a DB lock before mutating shared entities.","Shorten transaction scope so stale reads are less likely.","For async executors, increase retry settings and ensure the job handler is idempotent."],"exampleFix":"// before\nmanagementService.executeCommand(ctx -> { task.setName(name); return null; }); // may throw FlowableOptimisticLockingException\n// after\nfor (int i = 0; i < 3; i++) {\n  try { managementService.executeCommand(ctx -> { task.setName(name); return null; }); break; }\n  catch (FlowableOptimisticLockingException e) { task = taskService.createTaskQuery().taskId(task.getId()).singleResult(); }\n}","handlingStrategy":"validation","validationCode":"String stmt = dbSqlSessionFactory.getUpdateStatement(entity);\nif (dbSqlSessionFactory.mapStatement(stmt) == null) {\n  throw new IllegalStateException(\"No update statement mapped for \" + entity.getClass());\n}","typeGuard":null,"tryCatchPattern":"try { /* persist entity */ }\ncatch (FlowableException e) { if (e.getMessage().startsWith(\"no update statement\")) { /* add mapping and retry after deploy */ } throw e; }","preventionTips":["Always provide insert, update and delete statements together in custom entity mappers.","Include mapper XMLs in integration-test configuration so missing statements fail tests early.","Grep mapper XMLs for the statement ids the entity manager expects after upgrades.","Avoid renaming entity classes without updating statement registration."],"tags":["database","concurrency","optimistic-locking"],"backgroundTag":"optimistic-locking-conflict","analyzedSha":"d6d39ce1c69ff244f2d9dc6af756a9b95e865586","analyzedAt":"2026-09-11T06:41:19.413Z","contentChangedAt":"2026-09-11T06:41:19.413Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}