A bulk update processes 5,000 records. Validation rules reject 12 of them. The remaining 4,988 are clean. Should the 4,988 commit, or should the whole batch roll back?
The answer is business-specific. Sometimes partial success is correct (process what works, log the failures for manual review). Sometimes it is wrong (the records are interdependent and a partial commit leaves the data inconsistent). Apex offers three patterns to make the choice explicit.
The default behavior, when no pattern is chosen, is often the wrong default for production code.
The three Apex DML patterns
Pattern 1: simple DML keyword (insert, update, delete).
The all-or-nothing default. Any single record failing throws DmlException and rolls back the entire DML statement. The records that would have succeeded are not committed.
List<Account> accounts = buildLargeList();
insert accounts; // If any one fails, all roll back.
When this is right: when records are logically interdependent and partial commit would leave the org inconsistent.
When this is wrong: when records are independent and partial success is acceptable.
Pattern 2: Database method with allOrNone = true (explicit version of pattern 1).
Same behavior as the keyword, but the syntax surfaces the choice:
Database.SaveResult[] results = Database.insert(accounts, true);
// True = allOrNone. Throws if any fail.
Useful for clarity in code review. The true parameter announces "this is intentionally all-or-nothing." Future maintainers see the choice rather than inheriting an implicit default.
Pattern 3: Database method with allOrNone = false (partial success).
The DML continues even if some records fail. Returns results per record.
Database.SaveResult[] results = Database.insert(accounts, false);
// False = allow partial success.
for (Integer i = 0; i < results.size(); i++) {
Database.SaveResult sr = results[i];
if (!sr.isSuccess()) {
// Inspect the errors.
for (Database.Error err : sr.getErrors()) {
System.debug('Record ' + i + ' failed: ' +
err.getStatusCode() + ': ' + err.getMessage());
}
// Log to a custom error log object, alert, etc.
}
}
The shape: iterate the results array in the same order as the input list. Each result tells whether that record committed or failed, and why.
When this is right: bulk operations where partial completion is acceptable and failures are logged for manual review (e.g., a data import that processes 10,000 records and accepts 50 failures with reasons).
When this is wrong: any transaction where the records depend on each other or where downstream code assumes all-or-nothing semantics.
Savepoints for nested transaction control
For operations more complex than a single DML, Apex supports savepoints. A savepoint marks a transaction state that can be rolled back to.
Savepoint sp = Database.setSavepoint();
try {
// Multiple DML operations.
insert accounts;
insert contacts;
update opportunities;
// Some logic that may throw.
validateBusinessRules();
// If we reach here, commit happens at the end of the transaction.
} catch (Exception e) {
Database.rollback(sp);
// All DML since the savepoint is undone.
logError(e);
throw e; // Or handle gracefully.
}
The savepoint behaves like a transaction nested inside the outer Apex transaction. Multiple operations succeed or fail together; the outer transaction continues with the post-rollback state.
When savepoints help: complex business logic that needs all-or-nothing semantics across multiple DML calls. The simple keyword can't roll back work done before the failing DML; savepoints can.
When savepoints have limits: only one savepoint per top-level transaction is fully reliable in practice. Nested savepoints behave differently than developers from RDBMS backgrounds expect. Test the rollback semantics in the specific context.
A practical decision tree
For every DML in production Apex code:
Is this part of a transaction where records are logically interdependent? → Use the simple keyword or
Database.insert(records, true).Is this a bulk operation where some failures are acceptable? → Use
Database.insert(records, false). Process the SaveResult array. Log failures for review.Is this a multi-DML operation that should all succeed or all fail together? → Wrap in a try/catch with
Database.setSavepoint()andDatabase.rollback().Is this hidden inside a generic helper used by many callers? → Make the allOrNone choice a parameter. Let each caller decide.
Common DML mistakes
Five patterns Sapota has seen in audits:
1. Plain insert records; for bulk data loads.
A nightly batch processes 50,000 records. One fails on validation. All 50,000 roll back. Tomorrow night the same record fails again. The job has been "running" for 2 months but committing nothing.
Fix: use Database.insert(records, false). Log failed records for review.
2. Database.insert without inspecting SaveResult.
The code uses Database.insert(records, false) (partial success). Then ignores the returned array. Failures are silently dropped.
Fix: iterate the SaveResult and log every failure to a tracking object or platform event.
3. Loop with individual DML.
// Anti-pattern: DML inside loop.
for (Account a : accounts) {
try {
update a;
} catch (DmlException e) {
// ... handle per-record ...
}
}
This is the "per-record exception handling" pattern done wrong. It hits the 150-DML governor limit at 151 records. Use partial-success DML instead.
Fix: Database.update(accounts, false) plus SaveResult inspection.
4. Mixing allOrNone modes within one workflow.
Step 1 uses simple insert (all-or-nothing). Step 2 uses Database.insert(records, false) (partial). If step 1 fails, step 2 never runs. If step 2 has partial failures, the workflow is half-done. The mixed semantics produce hard-to-debug states.
Fix: pick one mode for the entire workflow. Use savepoints if you need atomic semantics across mixed operations.
5. Savepoint rollback without re-raising.
Savepoint sp = Database.setSavepoint();
try {
// ... DML ...
} catch (Exception e) {
Database.rollback(sp); // Rolled back.
// No re-raise. Caller thinks the operation succeeded.
}
The caller has no way to know the operation failed. Downstream code may proceed expecting the records to exist.
Fix: after rollback, either re-throw the exception or return a clear failure indicator to the caller.
Inspecting Database.Error
The Database.Error returned by SaveResult has a few useful properties:
getStatusCode(): the standard status code (REQUIRED_FIELD_MISSING, FIELD_CUSTOM_VALIDATION_EXCEPTION, etc.).getMessage(): a human-readable description.getFields(): which fields contributed to the error (if applicable).
For logging, capture all three:
for (Database.SaveResult sr : results) {
if (!sr.isSuccess()) {
Error_Log__c log = new Error_Log__c(
Record_Id__c = sr.getId(),
Status_Code__c = String.valueOf(sr.getErrors()[0].getStatusCode()),
Message__c = sr.getErrors()[0].getMessage(),
Fields__c = String.join(sr.getErrors()[0].getFields(), ',')
);
insert log;
}
}
The error log is the data set for triaging failures. Operations teams query it to see what is failing and why.
When DML is the wrong layer
Sometimes the right answer is not "different DML pattern" but "different architecture." Examples:
Bulk data imports. Use Bulk API, not Apex DML. The Bulk API is purpose-built for high-volume operations with native partial-success semantics. Apex DML is the wrong tool above 50K records.
Idempotent operations on individual records. Use upsert with an external ID. Lets the operation be safely retried without producing duplicates.
Distributed transactions across systems. Apex DML cannot roll back external system changes. Use a saga or 2-phase commit pattern across systems instead.
What good DML discipline looks like
A Salesforce org with healthy DML patterns:
- Every DML call has a documented allOrNone intent.
- Partial-success DML always inspects SaveResult and logs failures.
- Multi-DML workflows use savepoints when atomic semantics are needed.
- Bulk imports use Bulk API, not Apex DML.
- Error_Log__c (or equivalent) captures every failure with context.
- Code review explicitly checks the DML pattern selection.
Sapota's Salesforce team holds the Platform Developer I credential and treats DML pattern selection as a design decision on every engagement. The wrong choice is invisible until production hits a partial failure; the right choice handles those failures gracefully without losing data.
Designing bulk DML patterns or auditing existing Apex for partial-success handling? Sapota's Salesforce team handles DML architecture, error logging, and Bulk API integration on production engagements. Get in touch ->
See our full platform services for the stack we cover.








