Trigger Handler, what's its purpose?

TL;DR

The code you are being asked to "document" follows a Service Layer pattern. Methods in this approach tend to be static void and act on an input. Another common name for it I've seen is indeed Util or Utility, as you mention in your question.

I wrote quite a lot below about what I believe a Trigger Handler should be. How does it relate to the task you've been handed down? Well, for one, it is the basis for my contention that you should not follow the pattern laid out by this class. My main problem with it is that combining the Handler and Service layers just leads to confusion in your mental model and test suite.

Normally, documenting the Trigger and Handler layers is nigh trivial. And in fact, if you are good at descriptive naming, it may be fairly straightforward to translate the Handler implementation into sensible documentation.

You should very rarely need to write a 700 line Apex Class. I would say never, but the Apex Wrapper Salesforce Metadata API* is one example which has made me soften my stance there and back off from absolutes.


Benefits of the Trigger Handler Pattern

Related reading: Trigger Frameworks and Apex Trigger Best Practices

While my preferred pattern differs slightly, the above article lays out many of the advantages. I personally find a handler implementation must-have because it:

  • facilitates logic-less triggers and One Trigger Per Object, which itself has many benefits
    • (including fine grained control over order of execution)
  • improves readability and Separation of Concerns
    • (this improved readability makes version control more fruitful as well)
  • optionally facilitates selective disablement

Separation of Concerns

The concerns can be simply separated into three basic questions:

  1. When (trigger layer) - When will records be acted on?
  2. What/Which (handler layer) - What actions will be taken? Which records will be acted on?
  3. How (service layer) - How will those actions be performed? How are those criteria defined?

Trigger

The trigger can simply worry about when:

trigger MyObject on MyObject__c (/*events*/)
{
    TriggerHandler handle = new TriggerHandler(trigger.new, trigger.oldMap);
    if (trigger.isBefore)
    {
        if (trigger.isInsert) handle.beforeInsert();
        if (trigger.isUpdate) handle.beforeUpdate();
    }
    if (trigger.isAfter)
    {
        if (trigger.isInsert) handle.afterInsert();
        if (trigger.isUpdate) handle.afterUpdate();
    }
}

One thing I really like about this pattern is that you can very quickly look at your trigger on the object and identify which events have logic on them. They also will be very stable in your code repository over time.

Handler

The handler can worry about what/which:

public with sharing class MyObjectTriggerHandler
{
    @TestVisible static Boolean bypassTrigger = false;

    final List<MyObject__c> newRecords;
    final Map<Id, MyObject__c> oldMap;
    public MyObjectTriggerHandler(List<MyObject__c> newRecords, Map<Id, MyObject__c> oldMap)
    {
        this.newRecords = newRecords;
        this.oldMap = oldMap;
    }

    public void beforeInsert()
    {
        if (bypassTrigger) return;
        MyObjectService.validate(
            MyObjectService.isInvalid().filter(newRecords)
        );
    }
    public void afterInsert()
    {
        if (bypassTrigger) return;
        MyObjectService.alterRelatedData(
            MyObjectService.shouldAlterRelatedData().filter(newRecords)
        );
    }

    public void beforeUpdate()
    {
        if (bypassTrigger) return;
        MyObjectService.transform(newRecords);
    }
    public void afterUpdate()
    {
        if (bypassTrigger) return;
        MyObjectService.alterRelatedData(
            MyObjectService.shouldAlterRelatedData().filter(newRecords, oldMap)
        );
    }
}

The primary advantages I see in the above are that it again becomes easy to quickly identify the effects of any given trigger event, and defer comprehension of the how (the trickiest question) to smaller code units. It also has a side benefit of making selective disablement much easier to implement. Usually I just make the entire trigger possible to bypass for unit testing purposes (about which more later).

Service

The service can then finally answer those pesky how questions:

public with sharing class MyObjectService
{
    public static Select.Filter shouldAlterRelatedData()
    {
        // some filter
    }
    public static Select.Filter isInvalid()
    {
        // some filter
    }
    public static void validate(List<MyObject__c> records)
    {
        for (MyObject__c record : records) record.addError('message');
    }
    public static void transform(List<MyObject__c> records)
    {
        // modify records
    }
    public static void alterRelatedData(List<MyObject__c> records)
    {
        // modify parent/sibling/child records
    }
}

The main advantage I see in this service pattern is that unit testing becomes much more atomic. The Selector library really helps keep this layer from growing too large, and also has a great explanation of the testing benefits in its documentation.

In testing the Service Layer, sometimes I need to insert records so I can test methods that need to be re-queried. In such instances, I can get a clearer picture about the behavior of the service by turning off the triggers, which may call the same methods when active.

* The MetadataService class has over 11K LOC