How to Count Contacts on Each Account Using Trigger

Count related contacts on each Account in Salesforce using a custom Apex Trigger. A simple solution for real-time rollups.

This trigger runs after insert, update, or delete operations on the Contact object. It collects all relevant Account IDs and queries the number of Contacts for each, updating a custom field called Number_of_Contacts__c on the Account. This approach ensures your data stays up-to-date in real time without needing scheduled jobs or third-party tools like DLRS. Below is the complete code implementation, including the trigger, Apex class, and a test class to ensure coverage and maintainability.  

Trigger-

trigger noOfContactsTrigger on Contact (after insert, after update, after delete) {
	if (Trigger.isAfter) {
        if (Trigger.isInsert || Trigger.isUpdate || Trigger.isDelete) {
            ContactTriggerHandler.updateAccountContactCounts(Trigger.new, Trigger.oldMap);
        }
    }
}

Apex Class / Trigger Handler –

public class ContactTriggerHandler {
    public static void updateAccountContactCounts(List<Contact> newList, Map<Id, Contact> oldMap) {
        Set<Id> accountIds = new Set<Id>();
        
        // From new records
        if (newList != null) {
            for (Contact con : newList) {
                if (con.AccountId != null) {
                    accountIds.add(con.AccountId);
                }
            }
        }

        // From oldMap (for deletes/updates)
        if (oldMap != null) {
            for (Contact oldCon : oldMap.values()) {
                if (oldCon.AccountId != null) {
                    accountIds.add(oldCon.AccountId);
                }
            }
        }

        if (accountIds.isEmpty()) return;

        List<Account> accountsToUpdate = [
            SELECT Id, (SELECT Id FROM Contacts) 
            FROM Account 
            WHERE Id IN :accountIds
        ];

        for (Account acc : accountsToUpdate) {
            acc.Number_of_Contacts__c = acc.Contacts.size();
        }

        update accountsToUpdate;
    }
}

Test Class –

@isTest
public class ContactTriggerHandlerTest {
    
    @isTest
    static void testInsertContacts() {
        // Create an account
        Account acc = new Account(Name = 'Test Account');
        insert acc;

        // Insert 3 contacts for the account
        List<Contact> contacts = new List<Contact>{
            new Contact(FirstName = 'John', LastName = 'Doe', AccountId = acc.Id),
            new Contact(FirstName = 'Jane', LastName = 'Smith', AccountId = acc.Id),
            new Contact(FirstName = 'Mike', LastName = 'Jones', AccountId = acc.Id)
        };
        insert contacts;

        // Fetch the updated account
        Account updatedAcc = [SELECT Id, Number_of_Contacts__c FROM Account WHERE Id = :acc.Id];

        System.assertEquals(3, updatedAcc.Number_of_Contacts__c, 'Contact count should be 3 after insert');
    }

    @isTest
    static void testUpdateContactAccount() {
        // Create two accounts
        Account acc1 = new Account(Name = 'Account 1');
        Account acc2 = new Account(Name = 'Account 2');
        insert new List<Account>{ acc1, acc2 };

        // Create a contact under acc1
        Contact con = new Contact(FirstName = 'Sam', LastName = 'Wilson', AccountId = acc1.Id);
        insert con;

        // Move contact to acc2
        con.AccountId = acc2.Id;
        update con;

        // Check both accounts
        Map<Id, Account> accounts = new Map<Id, Account>(
            [SELECT Id, Number_of_Contacts__c FROM Account WHERE Id IN :new List<Id>{acc1.Id, acc2.Id}]
        );

        System.assertEquals(0, accounts.get(acc1.Id).Number_of_Contacts__c, 'Account 1 should have 0 contacts');
        System.assertEquals(1, accounts.get(acc2.Id).Number_of_Contacts__c, 'Account 2 should have 1 contact');
    }

    @isTest
    static void testDeleteContact() {
        // Create an account and a contact
        Account acc = new Account(Name = 'Delete Test Account');
        insert acc;

        Contact con = new Contact(FirstName = 'Delete', LastName = 'Me', AccountId = acc.Id);
        insert con;

        // Verify count before delete
        acc = [SELECT Id, Number_of_Contacts__c FROM Account WHERE Id = :acc.Id];
        System.assertEquals(1, acc.Number_of_Contacts__c);

        // Delete contact
        delete con;

        // Verify count after delete
        acc = [SELECT Id, Number_of_Contacts__c FROM Account WHERE Id = :acc.Id];
        System.assertEquals(0, acc.Number_of_Contacts__c);
    }
}

Have questions or want to explore more trigger scenarios. Feel Free to leave a comment or check out our other posts on apex trigger sceanrios.