Frequently Asked Salesforce Interview Triggers

Prevent users from deleting an Account if it has any related Opportunities that are not in Closed Won or Closed Lost stages

Note: By default, Salesforce has built-in standard behavior that prevents users from deleting an Account if it has any associated Opportunities. However, if you need to restrict this deletion specifically based on the Opportunity stage (i.e., allowing deletion for Closed status but blocking it for Open status), a custom trigger like the one below is required.

trigger AccountTrigger on Account (before delete) { 

    if (Trigger.isBefore && Trigger.isDelete) { 

        Set<Id> accIds = new Set<Id>(); 

        // Gather all Account IDs being deleted
        for (Account acc : Trigger.old) { 
            accIds.add(acc.Id);
        }

        // Find Accounts that have at least one open Opportunity
        Set<Id> accWithOpenOppr = new Set<Id>();
        for (Opportunity oppr : [SELECT AccountId FROM Opportunity 
                                 WHERE AccountId IN :accIds 
                                 AND StageName NOT IN ('Closed Won', 'Closed Lost')]) { 
            accWithOpenOppr.add(oppr.AccountId);
        }

        // Loop through the records in Trigger.old to apply the error
        for (Account acc : Trigger.old) { 
            if (accWithOpenOppr.contains(acc.Id)) { 
                // CRITICAL: .addError() must be called on the Account object itself
                acc.addError('This account cannot be deleted because it has open Opportunities.');
            }
        } 
    }
}

Note: This is a simplified version ideal for coding interviews. While most interviewers will accept this approach initially to see your fundamental logic, you should be prepared for follow-up questions regarding bulkification and how it handles multiple record operations.

trigger ContactTrigger on Contact(after insert, after delete) { 
    
    Set<Id> accId = new Set<Id>();
    
    if(Trigger.isInsert && Trigger.isAfter) { 
        for(Contact con: Trigger.new) { 
            if(con.AccountId != null) { 
                accId.add(con.AccountId);
            }
        }
    }

    if(Trigger.isDelete && Trigger.isAfter) { 
        for(Contact con: Trigger.old) { 
            if(con.AccountId != null) { 
                accId.add(con.AccountId);
            }
        }
    }

    if(!accId.isEmpty()) {
        List<Account> accToUpdate = [SELECT Id, Name, Contact_Count__c FROM Account WHERE Id IN :accId];

        for(Account acc: accToUpdate) { 
            Decimal currentContact = acc.Contact_Count__c == null ? 0 : acc.Contact_Count__c;
            
            if(Trigger.isDelete) { 
                acc.Contact_Count__c = currentContact - 1;
            }
            if(Trigger.isInsert) { 
                acc.Contact_Count__c = currentContact + 1;
            }
        }
        
        if(!accToUpdate.isEmpty()) { 
            update accToUpdate;
        }
    }
}

Note: This is the ideal, production-grade solution that interviewers look for. It effectively showcases your knowledge of handling the complete trigger lifecycle (including undelete and update account-swapping logic) and proves you know how to bulkify Apex properly using Aggregate SOQL queries instead of dangerous math increments inside loops.

trigger ContactTrigger on Contact (after insert, after update, after delete, after undelete) {

    Set<Id> accountIds = new Set<Id>();

    // 1. Gather affected Account IDs based on the trigger context
    if (Trigger.isInsert || Trigger.isUndelete) {
        for (Contact con : Trigger.new) {
            if (con.AccountId != null) {
                accountIds.add(con.AccountId);
            }
        }
    }
    
    if (Trigger.isDelete) {
        for (Contact con : Trigger.old) {
            if (con.AccountId != null) {
                accountIds.add(con.AccountId);
            }
        }
    }
    
    if (Trigger.isUpdate) {
        for (Contact con : Trigger.new) {
            Contact oldCon = Trigger.oldMap.get(con.Id);
            // If the Contact was moved from one Account to another, track BOTH accounts
            if (con.AccountId != oldCon.AccountId) {
                if (con.AccountId != null) accountIds.add(con.AccountId);
                if (oldCon.AccountId != null) accountIds.add(oldCon.AccountId);
            }
        }
    }

    // 2. Process and recalculate the counts
    if (!accountIds.isEmpty()) {
        
        // Pre-populate a map with 0 for all tracked accounts. 
        // This handles cases where the last remaining contact on an account is deleted.
        Map<Id, Account> accountsToUpdateMap = new Map<Id, Account>();
        for (Id accId : accountIds) {
            accountsToUpdateMap.put(accId, new Account(Id = accId, Contact_Count__c = 0));
        }

        // 3. Aggregate Query: Group by AccountId to get the exact real-time count
        List<AggregateResult> results = [SELECT AccountId, COUNT(Id) totalContacts 
                                         FROM Contact 
                                         WHERE AccountId IN :accountIds 
                                         GROUP BY AccountId];

        // 4. Overwrite the map values with the actual database totals
        for (AggregateResult ar : results) {
            Id accId = (Id)ar.get('AccountId');
            Integer count = (Integer)ar.get('totalContacts');
            
            accountsToUpdateMap.get(accId).Contact_Count__c = count;
        }

        // 5. Update the Accounts in the database
        update accountsToUpdateMap.values();
    }
}
trigger ContactTrigger on Contact (before delete) { 

    if (Trigger.isBefore && Trigger.isDelete) { 
        for (Contact con : Trigger.old) { 
            // Corrected the syntax error and typos here
            if (con.AccountId != null && con.isPrimary__c) { 
                con.addError('Cannot delete this Contact because it is marked as the Primary Contact on Account ID: ' + con.AccountId);
            }
        }
    }
}
trigger OpportunityTrigger on Opportunity (before delete) { 

    if (Trigger.isBefore && Trigger.isDelete) {
        Set<Id> oppIds = new Set<Id>(); 

        // 1. Gather all Opportunity IDs being deleted
        for (Opportunity opp : Trigger.old) { 
            oppIds.add(opp.Id); 
        }

        // 2. Query for Tasks where Status is exactly 'Open'
        Set<Id> oppsWithOpenTasks = new Set<Id>();
        
        for (Task tsk : [SELECT WhatId FROM Task WHERE WhatId IN :oppIds AND Status != 'Completed']) { 
            oppsWithOpenTasks.add(tsk.WhatId);
        }

        // 3. Prevent deletion by adding the error to the Opportunity record
        for (Opportunity opp : Trigger.old) { 
            if (oppsWithOpenTasks.contains(opp.Id)) { 
                opp.addError('Cannot delete this Opportunity because it has open tasks associated with it.');
            }
        }
    }
}
trigger AccountTrigger on Account (before delete) { 

    if (Trigger.isBefore && Trigger.isDelete) { 
    
        // 1. Get account IDs instantly without a loop
        Set<Id> accIds = Trigger.oldMap.keySet(); 

        // 2. Aggregate query
        List<AggregateResult> groupedResults = [
            SELECT AccountId, COUNT(Id) totalCount 
            FROM Contact 
            WHERE AccountId IN :accIds 
            GROUP BY AccountId 
            HAVING COUNT(Id) > 5
        ];

        Set<Id> accWithMoreThan5Contacts = new Set<Id>();

        // 3. Populate the set 
        for (AggregateResult ar : groupedResults) { 
            // The query already filtered for > 5, so we just grab the ID
            accWithMoreThan5Contacts.add((Id)ar.get('AccountId'));
        }

        // 4. Add error to block deletion (Fixed .contacts() to .contains())
        for (Account acc : Trigger.old) { 
            if (accWithMoreThan5Contacts.contains(acc.Id)) { 
                acc.addError('Cannot delete this Account because it has more than 5 related Contacts.');
            }
        }
    }
}

Leave a Reply