{"record":{"id":"d174dcc571a0ddb4","repo":"flowable/flowable-engine","slug":"persistentobject-was-updated-by-another-transact","errorCode":null,"errorMessage":"<persistentObject> was updated by another transaction concurrently","messagePattern":"<persistentObject> 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":240,"sourceCode":"\n        @Override\n        public void clearCache() {\n            cacheRemove(persistentObject.getClass(), persistentObject.getId());\n        }\n\n        @Override\n        public void execute() {\n            String deleteStatement = dbSqlSessionFactory.getDeleteStatement(persistentObject.getClass());\n            deleteStatement = dbSqlSessionFactory.mapStatement(deleteStatement);\n            if (deleteStatement == null) {\n                throw new ActivitiException(\"no delete statement for \" + persistentObject.getClass() + \" in the ibatis mapping files\");\n            }\n\n            // It only makes sense to check for optimistic locking exceptions for objects that actually have a revision\n            if (persistentObject instanceof HasRevision) {\n                int nrOfRowsDeleted = sqlSession.delete(deleteStatement, persistentObject);\n                if (nrOfRowsDeleted == 0) {\n                    throw new ActivitiOptimisticLockingException(persistentObject + \" was updated by another transaction concurrently\");\n                }\n            } else {\n                sqlSession.delete(deleteStatement, persistentObject);\n            }\n        }\n\n        public PersistentObject getPersistentObject() {\n            return persistentObject;\n        }\n\n        @Override\n        public String toString() {\n            return \"delete \" + persistentObject;\n        }\n    }\n\n    /**\n     * A bulk version of the {@link CheckedDeleteOperation}.","sourceCodeStart":222,"sourceCodeEnd":258,"githubUrl":"https://github.com/flowable/flowable-engine/blob/d6d39ce1c69ff244f2d9dc6af756a9b95e865586/modules/flowable5-engine/src/main/java/org/activiti/engine/impl/db/DbSqlSession.java#L222-L258","documentation":"Thrown by DbSqlSession's delete operation as ActivitiOptimisticLockingException when the DELETE affected zero rows for an entity implementing HasRevision. Zero rows means the row's revision no longer matches — another transaction already updated (or deleted) the entity concurrently, so this delete is based on stale state.","triggerScenarios":"Two concurrent transactions modifying the same entity (e.g. the same task, execution, or job) where one commits a new revision before the other issues its delete; deleting an entity that was concurrently updated; retries of stale command contexts.","commonSituations":"High-concurrency workflows with many users acting on the same task; parallel job executions touching the same process instance; long-running commands holding stale entity revisions across user think time; message/async executor contention on one execution.","solutions":["Catch ActivitiOptimisticLockingException and retry the whole command with fresh data — re-fetch the entity, re-apply, and delete again.","Serialize conflicting work: use exclusive jobs (async continuations with exclusive=true) or optimistic-lock-aware queues so one actor mutates an entity at a time.","Reduce transaction scope so entities are read and deleted within one short transaction.","If the entity was already deleted by the other transaction, treat the delete as idempotent and skip it."],"exampleFix":"// before\ntaskService.complete(taskId); // fails when another actor already updated the task\n// after\ntry {\n    taskService.complete(taskId);\n} catch (ActivitiOptimisticLockingException e) {\n    Task fresh = taskService.createTaskQuery().taskId(taskId).singleResult();\n    if (fresh != null) {\n        taskService.complete(taskId); // retry with fresh revision\n    }\n}","handlingStrategy":"retry","validationCode":"Task task = taskService.createTaskQuery().taskId(taskId).singleResult();\nif (task == null) {\n    return; // already removed concurrently — nothing to delete\n}","typeGuard":null,"tryCatchPattern":"int attempts = 3;\nwhile (attempts-- > 0) {\n    try {\n        doWorkWithEntity(id);\n        break;\n    } catch (ActivitiOptimisticLockingException e) {\n        if (attempts == 0) throw e;\n        // backoff, then re-fetch entity so the next attempt uses a fresh revision\n        sleep(50 * (3 - attempts));\n    }\n}","preventionTips":["Keep transactions short — re-read entities inside the same transaction that mutates them","Use exclusive async jobs for operations that contend on one process instance","Design deletes to be idempotent so a lost race is harmless","Set reasonable retry limits with backoff; alert if optimistic-lock retries spike"],"tags":["optimistic-locking","concurrency","database","transaction"],"backgroundTag":"conflicting-concurrent-update","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"}