[SalesForce] Maximum stack depth reached: 1001

I'm running into a stack depth issue which is puzzling at best, but maybe because I'm fairly new to Apex. I have an interface built to enforce some structure to trigger calls, which is then implemented by a virtual class, which begins building the correct constructors and executing the correct methods [takes place in the execute() method].

When I insert a new Opportunity record, it gives me a stack depth error UNLESS either of the following is true:

  1. I remove the "public void" methods [e.g. createScheduleLines()] and instead put that code directly in the "public override void" methods [e.g. OnAfterInsert()]
  2. I remove all "public void" methods that aren't "createScheduleLines()"

Why would this only work for one method reference and/or when there are no method references (but the code is built in)…

Is this because these "public void" methods need to be static so as to only be called when required?

Below is my code:

public interface triggerInterface {
    void execute();
    void onBeforeInsert();
    void onBeforeUpdate();
    void onBeforeDelete();
    void onAfterInsert();
    void onAfterUpdate();
    void onAfterDelete();
}

triggerBase implements triggerInterface

public virtual class triggerBase implements triggerInterface {

    protected boolean isBefore;
    protected boolean isAfter;
    protected boolean isInsert;
    protected boolean isUpdate;
    protected boolean isDelete;
    protected boolean isUndelete;

    public triggerBase(boolean isBefore, boolean isAfter, boolean isInsert, boolean isUpdate, boolean isDelete, boolean isUndelete) {
        this.isBefore = isBefore;
        this.isAfter = isAfter;
        this.isInsert = isInsert;
        this.isUpdate = isUpdate;
        this.isDelete = isDelete;
        this.isUndelete = isUndelete;
    }

    public triggerBase() {}

    //** Responsible for launching execution of business logic
    public virtual void execute() {
        if (this.isBefore) {
            if (this.isInsert) {
                System.debug('***** triggerBase.OnBeforeInsert()');
                this.onBeforeInsert();
            } else if (this.isUpdate) {
                System.debug('***** triggerBase.OnBeforeUpdate()');
                this.onBeforeUpdate();
            } else if (this.isDelete) {
                System.debug('***** triggerBase.OnBeforeDelete()');
                this.onBeforeDelete();
            }
        } else if (this.isAfter) {
            if (this.isInsert) {
                System.debug('***** triggerBase.OnAfterInsert()');
                this.onAfterInsert();
            } else if (this.isUpdate) {
                System.debug('***** triggerBase.OnAfterUpdate()');
                this.onAfterUpdate();
            } else if (this.isDelete) {
                System.debug('***** triggerBase.OnAfterDelete()');
                this.onAfterDelete();
            } else if (this.isUndelete) {
                System.debug('***** triggerBase.OnAfterUndelete()');
                this.onAfterUndelete();
            }
        }
    }

    public virtual void onBeforeInsert() {} //** Has access to newMap
    public virtual void onBeforeUpdate() {} //** Has access to oldMap and newMap
    public virtual void onBeforeDelete() {} //** Has access to oldMap
    public virtual void onAfterInsert() {} //** Has access to newMap
    public virtual void onAfterUpdate() {} //** Has access to oldMap and newMap
    public virtual void onAfterDelete() {} //** Has access to oldMap
    public virtual void onAfterUndelete() {} //** Has access to newMap
}

OpportunityTrigger then calls OpportunityHandler, which extends triggerBase

public class OpportunityHandler extends triggerBase {
    private Map<Id, Opportunity> newMap;
    private Map<Id, Opportunity> oldMap;
    private Boolean isHandlerExecuting = false;
    private Integer BatchSize = 0;
    public OpportunityHandler(Boolean isBefore, Boolean isAfter, 
            Boolean isInsert, Boolean isUpdate, Boolean isDelete, Boolean isUndelete, 
            Map<Id, Opportunity> oldMap, Map<Id, Opportunity> newMap, 
            Boolean isExecuting, Integer size) {
        super(isBefore, isAfter, isInsert, isUpdate, isDelete);
        this.newMap = newMap;
        this.oldMap = oldMap;
        this.isHandlerExecuting = isExecuting;
        this.BatchSize = size;
    }
    public override void OnAfterInsert() {
        System.debug('***** OpportunityHandler.OnAfterInsert()');
        this.createScheduleLines();
    }
    public override void OnAfterUpdate() {
        System.debug('***** OpportunityHandler.OnAfterUpdate()');
        this.adjustScheduleLines();
        //this.processDesignInClaim();
    }
    public override void OnBeforeDelete() {
        System.debug('***** OpportunityHandler.OnBeforeDelete()');
        this.removeScheduleLines();
    }
    //** When you insert an Opportunity(Socket), it should create a Schedule_Line__c for all current Opportunity(Socket)
    public void createScheduleLines() {
        System.debug('***** OpportunityHandler.OnAfterInsert().createScheduleLines()');
        //** Populate a list with all Schedule__c related to Opportunity(Board) where there are new Opportunity(Socket)
        List<Schedule__c> schs = [
            SELECT Id, Board_Name__c
            FROM Schedule__c
            WHERE Board_Name__c IN :this.newMap.keySet()];
        //** Create new Schedule_Line__c for each Schedule__c on the Opportunity(Board) where there are new Opportunity(Socket)
        List<Schedule_Line__c> schlnToInsert = new List<Schedule_Line__c>();
        for (Opportunity socket : this.newMap.values()) {
            for (Schedule__c sch : schs) {
                if (socket.Board_Name__c == sch.Board_Name__c) {
                    Schedule_Line__c newSL = new Schedule_Line__c();
                        newSL.Schedule__c = sch.Id;
                        newSL.Socket_Name__c = socket.Id;
                    schlnToInsert.add(newSL);
                }
            }
        }
        //** Insert all Schedule_Line__c where there are new Opportunity(Socket)
        List<Database.SaveResult> schlnToInsertResult = Database.insert(schlnToInsert, false);
    }
    //** When you update an Opportunity(Socket), it should force all related Schedule_Line__c.Value__c to re-calculate
    public void adjustScheduleLines() {
        System.debug('***** OpportunityHandler.OnAfterUpdate().adjustScheduleLines()');
        //** Populate a list with all Schedule__c where Opportunity.Socket_Value__c changed
        List<Id> socketIds = new List<Id>();
        for (Opportunity socket : this.newMap.values()) {
            if (socket.Socket_Value__c != this.oldMap.get(socket.Id).Socket_Value__c) {
                socketIds.add(socket.Id);
            }
        }
        //** Populate a list with all Schedule_Line__c where Opportunity.Socket_Value__c changed
        List<Schedule_Line__c> schlns = [
            SELECT Id
            FROM Schedule_Line__c
            WHERE Socket_Name__c IN :socketIds];
        //** Set Schedule_Line__c.Value__c to 0 to re-trigger the Workflow Rule which updates the value from the Opportunity(Socket)
        List<Schedule_Line__c> schlnToUpdate = new List<Schedule_Line__c>();
        for (Schedule_Line__c schln : schlns) {
            schln.Value__c = 0;
            schlnToUpdate.add(schln);
        }
        //** Update all Schedule_Line__c where Opportunity.Socket_Value__c changed
        List<Database.SaveResult> schlnToUpdateResult = Database.update(schlnToUpdate, false);
    }
    //** When you delete an Opportunity(Socket), it should delete all related Schedule_Line__c
    public void removeScheduleLines() {
        System.debug('***** OpportunityHandler.OnBeforeDelete().removeScheduleLines()');
        //** Populate a list with all Schedule_Line__c where Opportunity(Socket) is marked for deletion
        List<Schedule_Line__c> schlnToDelete = [
            SELECT Id
            FROM Schedule_Line__c
            WHERE Socket_Name__c IN :this.oldMap.keySet()];
        //** Delete all Schedule_Line__c where Opportunity(Socket) is marked for deletion
        List<Database.DeleteResult> schlnToDeleteResult = Database.delete(schlnToDelete, false);
    }
    /*
    //** When you update an Opportunity(Board) from Active to Closed it should update 'Design In' Opportunity(Socket)
    public void processDesignInClaim() {
        List<OpportunityLineItem> oliPrimary = new List<OpportunityLineItem>();
    }
    */
}

Best Answer

I am confused as to what you are really trying to accomplish with this structure. You don't need to create any logic for the actual order of execution (which, correct me if I am wrong, it appears you are trying to do here). Triggers have a very specific order of execution. It will actually run through your trigger several times to hit before and after logic.

Wouldn't it be much easier to do something like:

trigger OpportunityTrigger on Opportunity (after insert, after update, before delete) {
    if(Trigger.isAfter && Trigger.isInsert){
        OpportunityHandler.createScheduleLines(Trigger.newMap);
    }

    if(Trigger.isAfter && Trigger.isUpdate){
        OpportunityHandler.adjustScheduleLines(Trigger.newMap, Trigger.oldMap);
    }

    if(Trigger.isBefore && Trigger.isDelete){
        OpportunityHandler.removeScheduleLines(Trigger.oldMap);
    }

}

public class OpportunityHandler{

    public static void createScheduleLines(Map<Id, Opportunity> newMap) {
        // logic
    }

    public static void adjustScheduleLines(Map<Id, Opportunity> newMap, Map<Id, Opportunity> oldMap) {
        // logic
    }

    public static void removeScheduleLines(Map<Id, Opportunity> oldMap) {
        // logic
    }

}

Let Salesforce worry about the order they are executed.