Add a Savepoint to your Apex transaction so you can attempt a risky DML and roll back to a known good state if it fails, without aborting the entire transaction.
- Identify the rollback boundary
Pick the point in your code where the state is consistent and you would be willing to discard subsequent work if needed. This is usually right before a multi-step DML operation that might partially fail.
- Create the savepoint
Call Savepoint sp = Database.setSavepoint(); at the rollback boundary. The handle holds the transaction state at that point.
- Wrap the risky DML in try/catch
Try the DML operations you want to compensate. Catch the DmlException (or specific exception type). Inside the catch, call Database.rollback(sp); to revert to the savepoint state.
- Run compensating logic
After the rollback, optionally execute an alternative path: log the failure, insert a fallback record, or notify a queue. The transaction continues from the savepoint, not from the start.
- Test the partial rollback in apex tests
Write a unit test that forces the risky DML to fail (a validation rule, a unique constraint, a missing required field). Verify the records before the savepoint are still present and the compensating logic ran.
- Mind the SOQL counter
Remember that Database.rollback() does not reset the SOQL query counter. If your savepoint code ran many queries, you can still hit the per-transaction limit even after rolling back.
Handle returned by Database.setSavepoint(); marks a known good state to roll back to.
Parameter on Database.insert/update/delete that controls whether one bad row rolls back the whole batch. Independent from Savepoints.
Queueable, Future, Batch, Scheduled. Each runs in its own transaction with its own savepoint scope.
Class-level field that lives for one transaction. Useful for caching and recursion guards inside the transaction.
- Database.rollback(sp) does not reset the SOQL or DML statement counter. You can roll back and still exceed the transaction limit on retry.
- Uncaught exceptions roll back the entire transaction, even DML performed before a savepoint. Use try/catch deliberately at the boundary that needs to handle the failure.
- Future, Queueable, and Batch jobs queue from a transaction but execute in their own transactions later. Limits on the calling transaction do not transfer; limits on the callee transaction start fresh.
- Trigger order of execution causes recursive saves to look like the same transaction even though logically they are nested calls. Use a static boolean guard to prevent infinite recursion.