Service Layer - cdelfattore/apex-enterprise-patterns GitHub Wiki
The service layer is meant to provide a layer of separation between the context callers and the set of operations available in the service layer. A context caller could be a Apex Trigger, Lightning Web Component Controller, a Rest Service, Batch Apex, an Email Handler, etc. The service layer contains the application's business logic. The implementation of an applications business logic should not be implemented in a context caller, it should be implemented in the service layer. The code in the service layer should not be tied to a specific context caller.
Each object should have their own set of Service layer classes. Three Apex classes that will represent a single implementation for the service layer. For example, the Apex classes related to the service layer for Account would be AccountsService, AccountsServiceImpl, and IAccountsService.
- End the class name with the "Service" suffix.
- When creating a service class for an object, use the plural form of the object.
- The actual name of the service class can be pretty much anything, but is typically a major module or significant object in the application.
UtilService
AccountsHelper
BatchApexService
CalcOpportunityProbService
AccountsService
OpportunitiesService
CommonService
Method names should ideally relate to or use the terms expressed in the application's end user vocabulary. A method name should not be named in a way that ties the method to the context in which the method was called. The service method is not concerned in the way it was called, the method name should not reflect that.
AccountsService.handleScheduler
OpportunitiesService.mergeQuickActionClicked
ProductsService.dailyBatchUpdate
AccountsService.inactivateAccounts
OpportunitiesService.changeOwner
ProductsService.changeProductFamily
Have the name focus on what the parameter represents. When a Map is the parameter type, a good convention would be to include something about the key and the value of the map in the name, somethingABySomethingB. For sets, keep it simple, accountIds, productNames, etc. With consitent naming, a developer can now easily recognize a parameter is a map or a set without either being specified in the name.
Map<Id, Account> accountById;
Map<String, Product> productByName;
Set<Id> accountIds;
Set<String> productNames;
When designing methods on the service layer, consider using list or set parameters by default. This will help promote the bulkification of code throughout the application. We want to avoid loops calling the service level methods. There are exceptions and not all service methods need to accept a set or list of parameters. Instances were it is more complex and costly to bulkified a method should be avoided.
Inner classes can occasionally be used to represent parameter data that is only relevant to the service class. When naming an inner class, be sure not to repeat the parent class name. You will have to reference the inner class by doing ParentClass.InnerClass anyways, so it's best not to repeat it.
It is best to have all Service classes run in user mode. This can be opted out of if needed. It's best to have the lowest level of permission be the default, use the with sharing
keywords in the class definition to implement this.
Given the following method that belongs to a lightning web controller Apex class:
/**
* @description Return the last ten accounts that were modified by the current logged in user.
* @return `List<Account>`
*/
@AuraEnabled(cacheable=true)
public static List<Account> getAccountWithContactsList()
{
return [SELECT Id, Name, (SELECT Id, FirstName, LastName FROM Contacts)
FROM Account WHERE LastModifiedById = :UserInfo.getUserId() ORDER BY LastModifiedDate DESC LIMIT 10];
}
@AuraEnabled
public static void updateDesciptionAndContactCount(List<String> accountIds, String description)
{
//Map the accountIds to a set, this could be moved to the Service class
Set<Id> accountIds = new Set<Id>();
for(String accountId : accountIdList)
{
accountIds.add(accountId);
}
List<Account> accountsWithContacts = [SELECT Id, Name, (SELECT Id, FirstName, LastName FROM Contacts)
FROM Account WHERE Id IN :accountIds];
for(Account account : accountsWithContacts)
{
account.description = description;
if (account.Contacts != null)
{
account.Contact_Count__c = account.Contacts.size();
}
else
{
account.Contact_Count__c = 0;
}
}
//may need to create a new list since Contacts is instantiated.
update accountsWithContacts;
}
This controller class should be broken up to separate the logic into a service class and call domain and selector classes as well. May look something like the following:
/**
* @description
* @author cdelfattore
* @since
* @group Service
*/
public with sharing class AccountsServiceImpl implements IAccountsService
{
/**
* @description Service method to simply retrieve contacts by their parent account.
* @return `List<Account>`
*/
public List<Account> retrieveContactsByAccount(Set<Id> userIds, Integer amount)
{
return AccountsSelector.newInstance().selectByLastModifiedIdWithContacts(userIds, amount);
}
public void updateAccountDescriptionsAndContactCount(List<String> accountIdList, String description)
{
//Map the accountIds to a set, this could be moved to the Service class
Set<Id> accountIds = new Set<Id>();
for(String accountId : accountIdList)
{
accountIds.add(accountId);
}
updateAccountDescriptions(accountIds, description);
}
/**
* @description Example of combining to service method calls into one service method.
* Notice the passing of one UnitOfWork instance.
* @param accountIds
* @param description
*/
public void updateAccountDescriptionsAndContactCount(Set<Id> accountIds, String description)
{
fflib_ISObjectUnitOfWork uow = Application.UnitOfWork.newInstance();
IAccounts accountsDomain = Accounts.newInstance(AccountsSelector.newInstance().selectByIdWithContacts(accountIds));
updateAccountDescriptions(uow, accountsDomain, description);
updateAccountContactCounts(uow, accountsDomain);
uow.commitWork();
}
/**
* @description Method to update an account description given account ids and description.
* @param accountIds
* @param description
*/
public void updateAccountDescriptions(Set<Id> accountIds, String description)
{
fflib_ISObjectUnitOfWork uow = Application.UnitOfWork.newInstance();
IAccounts accountsDomain = Accounts.newInstance(AccountsSelector.newInstance().selectById(accountIds));
updateAccountDescriptions(uow, accountsDomain, description);
uow.commitWork();
}
/**
* @description Update the account desciprtion
* @param uow
* @param accountsDomain
* @param description
*/
public void updateAccountDescriptions(fflib_ISObjectUnitOfWork uow, IAccounts accountsDomain, String description)
{
accountsDomain.setDescription(description);
uow.registerDirty(accountsDomain.getAccounts());
}
public void updateAccountContactCounts(Set<Id> accountIds)
{
fflib_ISObjectUnitOfWork uow = Application.UnitOfWork.newInstance();
updateAccountContactCounts(uow, Accounts.newInstance(AccountsSelector.newInstance().selectByIdWithContacts(accountIds)));
uow.commitWork();
}
public void updateAccountContactCounts(fflib_ISObjectUnitOfWork uow, IAccounts accountsDomain)
{
accountsDomain.setContactCount();
uow.registerDirty(accountsDomain.getAccounts());
}
}
Notice the name of this file is AccountsServiceImpl and that it implements the IAccountsService interface. The class we will use to call methods in the AccountsServiceImpl will be the AccountsService class. See the below example for both files:
/**
* @description Service class for the Account object.
* @author cdelfattore
* @since 2024-08-26
* @group Service
*/
public with sharing class AccountsService
{
public static List<Account> retrieveContactsByAccount(Set<Id> userIds, Integer amount)
{
return service().retrieveContactsByAccount(userIds, amount);
}
public static void updateAccountDescriptionsAndContactCount(List<String> accountIdList, String description)
{
service().updateAccountDescriptionsAndContactCount(accountIdList, description);
}
public static void updateAccountDescriptionsAndContactCount(Set<Id> accountIds, String description)
{
service().updateAccountDescriptionsAndContactCount(accountIds, description);
}
public static void updateAccountDescriptions(Set<Id> accountIds, String description)
{
service().updateAccountDescriptions(accountIds, description);
}
public static void updateAccountDescriptions(fflib_ISObjectUnitOfWork uow, IAccounts accountsDomain, String description)
{
service().updateAccountDescriptions(uow, accountsDomain, description);
}
public static void updateAccountContactCounts(Set<Id> accountIds)
{
service().updateAccountContactCounts(accountIds);
}
public static void updateAccountContactCounts(fflib_ISObjectUnitOfWork uow, IAccounts accountsDomain)
{
service().updateAccountContactCounts(uow, accountsDomain);
}
private static IAccountsService service()
{
return (IAccountsService) Application.Service.newInstance(IAccountsService.class);
}
}
/**
* @description Service interface for the Account object.
* @author
* @since
* @group Service
*/
public interface IAccountsService
{
List<Account> retrieveContactsByAccount(Set<Id> userIds, Integer amount);
void updateAccountDescriptionsAndContactCount(List<String> accountIdList, String description);
void updateAccountDescriptionsAndContactCount(Set<Id> accountIds, String description);
void updateAccountDescriptions(Set<Id> accountIds, String description);
void updateAccountDescriptions(fflib_ISObjectUnitOfWork uow, IAccounts accountsDomain, String description);
void updateAccountContactCounts(Set<Id> accountIds);
void updateAccountContactCounts(fflib_ISObjectUnitOfWork uow, IAccounts accountsDomain);
}
The lightning web component controller would then call the service method like the following:
@AuraEnabled(cacheable=true)
public static List<Account> getAccountWithContactsList()
{
return AccountsService.retrieveAndUpdateContactsByAccounts(new Set<Id>{UserInfo.getUserId()}, 10);
}
/**
* @description
* @param accountIds
* @param description
*/
@AuraEnabled
public static void updateDesciptionAndContactCount(List<String> accountIdList, String description)
{
AccountsService.updateAccountDescriptionsAndContactCount(accountIdList, description);
}
Notice there is a private static method called service() in the AccountsService class. In it, there is a reference to the Application class. The Application class is where you specify what Domain, Selector and Service classes are available in the application. The Application class will use it's respective Factory method in the fflib_Application class. The below code will enable the AccountsService class in the application. The most updated versions of the IAccounts, AccoutsService and AccountsServiceImpl can be found in this repository.
// Configure and create the ServiceFactory for this Application
public static final fflib_Application.ServiceFactory Service =
new fflib_Application.ServiceFactory(
new Map<Type, Type> {
IAccountsService.class => AccountsServiceImpl.class
IOpportunitiesService.class => OpportunitiesServiceImpl.class,
IInvoicingService.class => InvoicingServiceImpl.class
});
Full example of an Application class can be found here.
I hope this was a good example of removing the logic from a context caller. In this example the context caller is the lightning web component controller. AccountsService is a separate class that instantiates new instances of the AccountsServiceImpl class, and methods in the AccountsServiceImpl class.