{"record":{"id":"b9e1935f08e2e156","repo":"flowable/flowable-engine","slug":"updatedobject-was-updated-by-another-transaction","errorCode":null,"errorMessage":"<updatedObject> was updated by another transaction concurrently","messagePattern":"<updatedObject> was updated by another transaction concurrently","errorType":"exception","errorClass":"ActivitiOptimisticLockingException","httpStatus":null,"severity":"error","filePath":"modules/flowable5-engine/src/main/java/org/activiti/engine/impl/db/DbSqlSession.java","lineNumber":831,"sourceCode":"                    revisionEntity.setRevision(revisionEntity.getRevisionNext());\n                }\n            }\n        }\n    }\n\n    protected void flushUpdates(List<PersistentObject> updatedObjects) {\n        for (PersistentObject updatedObject : updatedObjects) {\n            String updateStatement = dbSqlSessionFactory.getUpdateStatement(updatedObject);\n            updateStatement = dbSqlSessionFactory.mapStatement(updateStatement);\n\n            if (updateStatement == null) {\n                throw new ActivitiException(\"no update statement for \" + updatedObject.getClass() + \" in the ibatis mapping files\");\n            }\n\n            LOGGER.debug(\"updating: {}\", updatedObject);\n            int updatedRecords = sqlSession.update(updateStatement, updatedObject);\n            if (updatedRecords != 1) {\n                throw new ActivitiOptimisticLockingException(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        updatedObjects.clear();\n    }\n\n    protected void flushDeletes(List<DeleteOperation> removedOperations) {\n        boolean dispatchEvent = false;\n        FlowableEventDispatcher eventDispatcher = Context.getProcessEngineConfiguration().getEventDispatcher();\n        if (eventDispatcher != null && eventDispatcher.isEnabled()) {\n            dispatchEvent = eventDispatcher.isEnabled();\n        }\n","sourceCodeStart":813,"sourceCodeEnd":849,"githubUrl":"https://github.com/flowable/flowable-engine/blob/d6d39ce1c69ff244f2d9dc6af756a9b95e865586/modules/flowable5-engine/src/main/java/org/activiti/engine/impl/db/DbSqlSession.java#L813-L849","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: unconditional second write\nexecution.setName(\"b\"); // execution was concurrently updated elsewhere\ncontext.getDbSqlSession().flushUpdates();\n\n// after: retry on optimistic lock\ntry {\n  execution.setName(\"b\");\n  flushUpdates();\n} catch (ActivitiOptimisticLockingException e) {\n  Execution fresh = runtimeService.createExecutionQuery().executionId(id).singleResult();\n  fresh.setName(\"b\");\n  save(fresh);\n}","handlingStrategy":"try-catch","validationCode":"// Optionally verify the revision hasn't moved before updating\nHasRevision e = entity;\nif (revisionInDb(e.getId()) != e.getRevision()) {\n    reload(entity); // refresh snapshot before mutating\n}","typeGuard":null,"tryCatchPattern":"try {\n    entity.setName(\"new\");\n    save(entity);\n} catch (ActivitiOptimisticLockingException e) {\n    // someone else updated first: re-read, re-apply, retry\n    Object fresh = reloadAndReapply(entity.getId());\n    save(fresh);\n}","preventionTips":["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."],"tags":["database","optimistic-locking","concurrency","update"],"backgroundTag":"optimistic-lock-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"}