Service Layer - wimvelzeboer/fflib-apex-extensions GitHub Wiki

Contents

A service layer is the main place that contains the high level business logic at its level of abstraction. It has business logic which is independent from the caller, meaning that every "entry point" should be able to invoke all service methods.

service

This means that the arguments accepted by the service method are in principle always in bulk. It should not accept entry point specific data, e.g. a WebService Data-Class which was generated based on a JSON.

The importance of Service classes

Writing and organising the service layer is one of the most important things in the Apex Enterprise Patterns. When done correctly it should be easy for the reader to understand the business logic.

Writing readable code is especially important for service classes, as it contains the most complexity in relation to all other classes.
Remember, developers spend 95% of their time reading code and just only 5% writing new code. Therefore, code should be very readable as every minute spend in cleaning up code to improve readability will save time. Do take same extra time to do some clean up after your logic works, and don’t try to skip this step to meet a dead-line. That will punch you strait back in your face the next time you have to read it. Do not think that it will slow you down, because next time you will be must faster.

Let’s do that math:
If you could spend 1% of your time on top op writing new code and thereby save just one tenth of your reading time, that would result in:

5% writing + 1% cleaning + 95% reading - 1/10 of 95% less reading =
5% + 1% + 95% - (95% * 0.1) =
6% + 95% - 9.5% =
6% + 85.5% = 91.5%

Wow, you saved 8.5% of your time by spending just 1% extra!

One difference between a smart programmer and a professional programmer is that the professional understands that clarity is king. Professionals use their powers for good and write code that others can understand. ― Robert C. Martin, Clean Code: A Handbook of Agile Software Craftsmanship

How to write a Service class method

Service methods should read like a book and invoke other methods with names describing exactly what they are doing. A service method would go through a process like the following below.

TO copy the shipping country from the account to the contacts
WE get the ShippingCountry values from the Accounts
THEN WE get the Account Record Ids
AND get the related contract records for the accounts
WE set the MailingCountry value on the Contacts by their Accounts value
THEN send the changes to the database
AND finally send an email to the contacts to inform them about this change
Note
Remember a service method is not related to its caller, so separate that from the actually business logic.
When and how this logic is invoked, is from the perspective of a service class completely irrelevant!

As you can see here the "TO paragraph" is written in a linear way, top to bottom, and sticks to one level of abstraction. When writing a service method it is always good to start writing this "TO paragraph" first, it can help you a great deal with structuring your source-code.

At this level of abstraction we are not interested in the how. We don’t care about how we get the ShippingCountry from the Accounts, nor are we interested in how we get the related contact records or where they come from. We also do not care about how those emails are send.
All this "how" is something another method at a slightly lower level of abstraction should care about.

Applying this way of working allows you to quickly write a small method and finish that method at that particular level of abstraction. And then you can move onto the next method at that other level of abstraction, repeating the same pattern over and over again until you are at a level where the method is small and clear and just doing a single operation.

Let’s break it down into code
When we take the "TO paragraph" and translate it into code, it could look something like here below.

// TO copy the shipping country from the account to the contacts
public void copyShippingCountryToContacts(...)
{
    // WE get the ShippingCountry values from the Accounts
    Accounts => getShippingCountry(...)

    // THEN WE get the Account Record Ids
    Accounts => getRecordIds(...)

    // AND get the related contract records for the accounts
    ContactsSelector.selectByAccountId(...)

    // WE set the MailingCountry value on the Contacts by their Accounts value
    Contacts => setMailingCountry( ... )

    // THEN send the changes to the database
    unitOfWork.registerDirty( Contacts );

    // AND finally send an email to the contacts to inform them about this change
    Emailservice  sendMailingCountryChangedEmail(...)
}

There are still a lot of blanks to fill in, but you can see clearly how the lines of the "TO paragraph" are translated. Let’s go a bit further and fill in the blanks;

// WE get the ShippingCountry values from the Accounts
Map<Id, String> shippingCountryByAccountId = accounts.getShippingCountryById();

The first step is to finalise the method call. Ask yourself where does it originate from and what input (arguments) should it require to do its thing.
We know that that data comes from Account records. Since we are dealing with multiple accounts we are dealing with a Domain, as its the purpose of a domain to encapsulate data (a lists records) and to provide access to that data.
We use a variable named accounts, that should hold an instance of the Accounts domain containing the account records.

We are dealing with many account records, and every account can have its own ShippingCountry value, therefore we need a map to store those values. We assign that data to a variable named shippingCountryByAccountId. The name is completely matching to what it contains, it is not telling its purpose since it has no purpose, its just data.
So, a name like countryValuesForContacts would be very inappropriate.

Note
We are returning a map with primitive data types, to decouple the data from its context/references.
// THEN WE get the Account Record Ids
Set<Id> accountIds = accounts.getRecordIds();

In the second line we use a getter method on the Accounts domain to extract the record Ids and store them in a Set. Again we name the variable to what it contains accountIds.

// AND get the related contract records for the accounts
IContacts contacts = Contacts.newInstance(
    ContactsSelector.newInstance().selectByAccountId(accountIds)
);

Here we use the method selectByAccountId on the ContactsSelector class and pass that onto a Contacts domain.

// WE set the MailingCountry value on the Contacts by their Accounts value
contacts.setMailingCountryByAccountId(shippingCountryByAccountId);

Here set the country values from the accounts to the contacts for those accounts.

// THEN send the changes to the database
unitOfWork.registerDirty(contacts.getRecords());

We use the unit-of-work to register the changed records are dirty (for update).

// AND finally send an email to the contacts to inform them about this change
Emailservice.sendNotificationOfChangedMailingCountry(contacts);

And finally we need to send a notification email to the contacts that their MailingCountry is updated.

Now lets have a look at the result of this all:

Map shippingCountryByAccountId = accounts.getShippingCountryById();
Set accountIds = accounts.getRecordIds();
IContacts contacts = Contacts.newInstance(
		ContactsSelector.newInstance().selectByAccountId(accountIds)
);
contacts.setMailingCountryByAccountId(shippingCountryByAccountId);

unitOfWork.registerDirty(contacts.getRecords());

You can still clearly read the "TO paragraph" but then translated into the syntax of code.

Let’s finish this up and write the method header and their overloads

public void copyShippingCountryToContacts(Set accountIds)
{
    copyShippingCountryToContacts(Accounts.newInstance(accountIds));
}

public void copyShippingCountryToContacts(List records)
{
    copyShippingCountryToContacts(Accounts.newInstance(records));
}

public void copyShippingCountryToContacts(IAccounts accounts)
{
    fflib_ISObjectUnitOfWork unitOfWork = Application.UnitOfWork.newInstance();
    copyShippingCountryToContacts(unitOfWork, accounts);
    unitOfWork.commitWork();
}

public void copyShippingCountryToContacts(fflib_ISObjectUnitOfWork unitOfWork, IAccounts accounts)
{
    Map shippingCountryByAccountId = accounts.getShippingCountryById();
    Set accountIds = accounts.getRecordIds();
    IContacts contacts = Contacts.newInstance(
		    ContactsSelector.newInstance().selectByAccountId(accountIds)
    );
    contacts.setMailingCountryByAccountId(shippingCountryByAccountId);

    unitOfWork.registerDirty(contacts.getRecords());
}

A number of method overload were added to allow for different entry point. A UI Controller (Lightning/VisualForce page), might already have the record available and can use copyShippingCountryToContacts(List<Account> records), while an inbound REST API webservice request might just only have the record Ids and want to use copyShippingCountryToContacts(Set<Id> accountIds).

Note
At this stage you do not thing about your own implementation, which might just be that Lightning Web Component, but think about all the different (future) entry points that might want to re-use the business logic.
Important
Notice how the creation of the UnitOfWork and the call to commitWork are done in a separate method overload. It is always separated with its own method, from the actual business logic.

Basic Service Layer rules

  1. Service methods only contains high level business logic.
    It is not interested in the When and How, only the What is important.
    What steps needs to happen for completing the business logic.

  2. The logic is cross domains and combines one or more selector, services and domain layer classes.

  3. Methods do not reference any SObjectFields nor SObjectTypes.
    Use Domains and primitive data types to pass data around within and between the service methods.

  4. Method overloading is used to allow for different entry points to invoke the logic.
    A (public) service method that doesn’t have method overloads, does not exist.

  5. Typically, they do not contain iterations or if conditions.
    In exceptional cases they can be encapsulated in private methods, this typically happens for example when dealing with variables (maps) linking a key to a Domain and having to call an operation on each domain.

  6. The UnitOfWork is used for performing DML operations and is passed around to underlying services.

Naming conventions

Class names

Use the plural name of your Custom Object appended with 'Service' for the name of your Service class.

  // Bad examples
  RaceService.cls
  ServiceForRace.cls

  // Good examples
  RacesService.cls
  TeamsService.cls

Methods

The public methods of the service layer should primarily accept a list of id’s. To enable service methods to be called from another service layer we typicaly split the method into two parts. The first method only accepts a list of id’s, creates the UnitOfWork instance and fetches the records. The second method accepts the UnitOfWork and the list of record so that it can also be used in a greater context re-using the UnitOfWork.

  // Bad
  awardChampionshipPoints();
  awardChampionshipPoints(List<Race__c> raceRecords);

  // Good
  awardChampionshipPoints(Set<Id> raceIds);
  awardChampionshipPoints(fflib_ISObjectUnitOfWork uow, List<Race__c> races);

Method types

Business logic method

These methods are the core of the application, they perform the high level business logic. This logic is separated from their execution context, therefore they can be executed from any execution context.

Setter methods

Setter methods can also occur on a Service layer, the difference between a domain setter is that these methods perform database operations and their signature is slightly different

public void setRating(Set<Id> ids, String rating);
public void setRating(List<Account> records, String rating);
public void setRating(IAccounts accounts, String rating);
public void setRating(fflib_ISObjectUnitOfWork unitOfWork, IAccounts accounts, String rating);

Service class Template

A service layer consists of mainly three classes. One class acts as an entry point where the service methods can be easily invoked. It contains only static methods and has a private constructor to prevent instantiation. The interface class defines all the method signatures of the service class. And finally there is the implementation class, where the actual logic resides.

public with sharing class AccountsService
{
    private AccountsService() {}

    public static void recalculateRating(Set<Id> accoundIds)
    {
        service().recalculateRating(accountIds);
    }

    public static void recalculateRating(List<Account> records)
    {
        service().recalculateRating(records);
    }

    public static void recalculateRating(IAccounts accounts)
    {
        service().recalculateRating(accounts);
    }

    private static IAccountsService service()
    {
        return (IAccountsService) Application.Service.newInstance(IAccountsService.class);
    }
}
public interface IAccountsService
{
    void recalculateRating(Set<Id> accoundIds);
    void recalculateRating(List<Account> records);
    void recalculateRating(IAccounts accounts);
}
public with sharing class AccountsServiceImp extends IAccountsService
{
    public void recalculateRating(Set<Id> accoundIds)
    {
        recalculateRating(
            Accounts.newInstance(accountIds)
        );
    }

    public void recalculateRating(List<Account> records)
    {
        recalculateRating(
            Accounts.newInstance(records)
        );
    }

    public void recalculateRating(IAccounts accounts)
    {
        accounts
            .recalculateRating()
            .setRating();

        fflib_ISObjectUnitOfWork unitOfWork = Application.UnitOfWork.newInstance();
        unitOfWork.registerDirty(accounts);
        unitOfWork.commitWork();
    }
}

Domain Method The service method will call a method on the domain performing the low level business logic.

public Accounts recalculateRating()
{
    selectByNumberOfEmployeesGreaterThan(50)
            .setRating('Warm');

    selectByNumberOfEmployeesLessThan(50)
            .setRating('Cold');

    return this;
}

Alternative implementation

To avoid having the AccountsService class which duplicates the method signatures we could use the main domain class for creating the instances

public with sharing class Accounts extends fflib_SObjects implements Accounts
{
    public static IAccountsService Service
    {
        get
        {
            return (IAccountsService) Application.Service.newInstance(IAccountsService.class);
        }
    }
}

Usage

Accounts.Service.recalculateRating(accountRecords);
⚠️ **GitHub.com Fallback** ⚠️