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.