[readonly] markdown buffer
Partial-Success Apex: Database Methods Done Properly
Partial-success DML sounds simple:
Database.SaveResult[] results = Database.insert(records, false);
Set allOrNone to false, let valid records succeed, and handle the failures.
The danger is that last phrase. If every SaveResult is not turned into a useful outcome, partial success becomes silent partial failure. That is worse than a transaction that clearly failed.
First decide whether partial success is valid
Atomicity is a business requirement, not an exception-handling preference.
An invoice header and its lines probably form one invariant. Keeping half an invoice is not success. A nightly repair across unrelated Accounts is different: one invalid Account should not block the rest.
Use partial DML when records are independent, item-level outcomes make sense, and retries have defined semantics. Keep all-or-nothing behaviour when the collection represents one business transaction.
- headervalid
- line 01invalid tax code
- line 02valid
- account Asaved
- account Bfailed validation
- account Csaved
allOrNone=false does not handle errors. It gives your code permission to receive several outcomes.
Return a ledger for every input
Database results correspond to the submitted record order. Preserve that relationship until every input has an explicit outcome.
Database.SaveResult[] saveResults = Database.insert(accounts, false);
List<RecordOutcome> outcomes = new List<RecordOutcome>();
for (Integer index = 0; index < saveResults.size(); index++) {
Database.SaveResult result = saveResults[index];
Account submitted = accounts[index];
outcomes.add(
result.isSuccess()
? RecordOutcome.succeeded(
submitted.External_Key__c,
result.getId()
)
: RecordOutcome.failed(
submitted.External_Key__c,
toProblems(result.getErrors())
)
);
}
Do not sort results or rebuild them from successful IDs before correlation is complete. New records may not have an ID, so carry a source row, client key, or external key.
A result can contain several errors. Preserve the status code, message, and fields rather than keeping only the first one.
public class RecordOutcome {
public String sourceKey;
public Boolean success;
public Id recordId;
public List<RecordProblem> problems;
}
Every submitted item should receive exactly one outcome. The caller should never infer failure from an ID missing from a shorter success list.
-
00ACME-01→success001…A1→ACME-01saved
-
01ACME-02→failedREQUIRED_FIELD→ACME-02retry after correction
-
02ACME-03→success001…A3→ACME-03saved
Partial does not mean isolated
Each record can still invoke validation rules, Flows, triggers, roll-ups, and downstream automation. Governor limits are shared across the transaction. Partial DML is not a collection of tiny transactions.
Some failures can be found before DML, such as a missing source key or duplicate request. Validate those first, then run one collection DML operation for the remaining candidates. Do not duplicate every Salesforce validation rule; database errors remain part of the contract.
Retries need equal care. Retrying the original collection can repeat records that already succeeded. Retry failed outcomes only, or use external IDs and idempotency keys where they match the domain.
There is one more trap: a successful SaveResult is not a durable commit while the Apex transaction is still running. A later unhandled exception can roll everything back. If each unit needs an independent commit boundary, use separate transactions.
Finish with proof, not optimism
The essential test mixes valid and invalid records. Assert that valid records were saved, invalid records were not, every input has one correlated outcome, all relevant errors are captured, and retry logic selects only safe failures.
Partial-success DML is useful when it returns a complete ledger: what was attempted, what succeeded, what failed, why it failed, and what may happen next.
Without that ledger, allOrNone=false only makes failure quieter.
Quiet failure is not resilience.