Trace's Four Lifecycle Methods
Trace's Four Lifecycle Methods
The Trace class exposes four lifecycle methods that express the flow of a Usecase.
Hold it in an instance field
Create the Trace exactly once, as an instance field on the Usecase class.
public with sharing class CopyAccountIndustryToOpportunityUsecase {
private Trace t = Trace.of('Copy the parent account industry to the opportunity');
// ...
}
Why once, and why an instance field, is covered in Nested traces and the two modes. For now it is enough to remember: one Trace per Usecase.
Four methods, three exit paths
| Method | Purpose | When to call it |
|---|---|---|
t.start() | Processing begins | At the top of invoke() |
t.log(msg) | An intermediate note | Anywhere, any number of times |
t.skip(msg) | Exit by skipping | Right before an early return (nothing to process, etc.) |
t.abort(msg) | Exit abnormally | When bailing out on an exception or a business error |
t.finish(msg) | Exit normally | At the end of invoke() |
log / skip / abort / finish all have a no-argument overload. When there is no message worth keeping — you just want to close the context — call t.finish(). That fits cases where only the path (skip / finish / abort) matters and no extra log line is needed.
The important part is the three exit paths.
finish: the work completed normallyskip: nothing to do (no targets, conditions not met) and it exited normallyabort: it did not complete, because of an exception or a business error
Keep these three distinct and TraceFlow can later assert which path a Usecase took.
Example
Here is CopyAccountIndustryToOpportunityUsecase, viewed through the lens of Trace.
public with sharing class CopyAccountIndustryToOpportunityUsecase {
@TestVisible static final String LBL_FETCH = 'oppFetch';
@TestVisible static final String LBL_UPDATE = 'oppUpdate';
private final Set<Id> opportunityIds;
private final IEloquent eloquent;
private Trace t = Trace.of('Copy the parent account industry to the opportunity');
// (constructors omitted — see step 3 of the introduction guide)
public void invoke() {
this.t.start();
if(this.opportunityIds == null || this.opportunityIds.isEmpty()) {
this.t.skip('No target opportunities, exiting.');
return;
}
Scribe oppScribe = Scribe.of(Opportunity.class)
.field('Id')
.parentField(Scribe.asParent('AccountId').field('Industry'))
.whereIn('Id', this.opportunityIds);
List<IEntry> oppEntries = this.eloquent.label(LBL_FETCH).get(oppScribe);
for(IEntry oppEntry : oppEntries) {
IEntry accountEntry = oppEntry.getParent('AccountId');
oppEntry.put('Industry__c', accountEntry.get('Industry'));
}
this.eloquent.label(LBL_UPDATE).doUpdate(oppEntries);
this.t.finish('Copied the industry onto ' + oppEntries.size() + ' opportunities.');
}
}
This Usecase ends on two paths: skip when the target set is empty, finish once the work is done. The next chapter shows how to tell those two apart in a test.
Related Documents
- Verifying execution paths with TraceFlow: pin the recorded path in a test
- Nested traces and the two modes: the ordering rule when a Usecase calls a Usecase
- ApexTrace guide: back to the guide index