Skip to main content
01 Separation of ConcernsChapter 07 of 2020 Force DI
Chapter 07 · The Service layer

The Service Layer

The layer where your business logic lives.

10 min readVideo 24:08Standard template

What is the Service Layer?

The Service Layer "defines an application's boundaries with a layer of services that establishes a set of available operations and coordinates the application's response in each operation". - Martin Fowler

This essentially just means that the service layer should house your business logic. It should be a centralized place that holds code that represents business logic for each object (database table) or the service layer logic for a custom-built app in your org (more common when building managed packages).

Difference between the Service Layer and Domain Layer - People seem to often confuse this layer with the Domain layer. The Domain layer is only for object-specific default operations (triggers, validations, updates that should always execute on a database transaction, etc). The Service layer is for business logic for major modules/applications in your org. Sometimes that module is represented by an object, sometimes it is represented by a grouping of objects. Domain layer logic is specific to each individual object whereas services often are not.


Service Layer Naming Conventions

Class Names - Your service classes should be named after the area of the application your services represent. Typically services classes are created for important objects or applications within your org.

Service Class Name Examples (Note that the writer of this guide prefers underscores in class names, this is just personal preference):

Account_Service
DocumentGenerationApp_Service

Method Names - The public method names should be the names of the business operations they represent. The method names should reflect what the end users of your system would refer to the business operation as. Service layer methods should also ideally always be static.

Method Parameter Types and Naming - The method parameters in public methods for the service layer should typically only accept collections (Map, Set, List) as the majority of service layer methods should be bulkified (there are some scenarios however that warrant non-collection types). The parameters should be named something that reflects the data they represent.

Service Class Method Names and Parameter Examples:

public static void calculateOpportunityProfits(List<Account> accountsToCalculate)
public static void generateWordDocument(Map<String, SObject> sObjectByName)

Service Layer Security

First, a piece of context that surprises most developers new to the platform (and plenty of seasoned ones from other stacks): "sharing" is an end-user-friendly name for what an engineer would call row-level security, and it applies both inside and outside of code (reports respect it too). More importantly, Apex runs with sharing by default - sharing enforcement is something the developer can set/alter by annotating classes at design time. It's not a runtime setting someone can flip later. By default (as of Salesforce API v67.0) with sharing is on by default and record access is respected unless you alter that in the class's declaration. That said sharing still needs to be a deliberate part of your layer design rather than an afterthought.

Service Layer Security Enforcement - Service layers hold business logic so by default they should at minimum use inherited sharing when declaring the classes, however I would suggest always using with sharing and allowing developers to elevate the code to run without sharing when necessary by using a private inner class. Declaring it on the service does double duty: because the service is the entry point into your business logic, it sets the sharing context for all the domain and selector code executed downstream of it.

Example Security for a Service Layer Class:

public with sharing class Account_Service{
public static void calculateOpportunityProfits(List<Account> accountsToCalculate){
//code here
new Account_Service_WithoutSharing().calculateOpportunityProfits_WithoutSharing(accountsToCalculate);
}

private without sharing class Account_Service_WithoutSharing{
public void calculateOpportunityProfits_WithoutSharing(List<Account> accountsToCalculate){
//code here
}
}
}

Notice the inner class: when you genuinely need without sharing, keep its scope as short and contained as possible - a private inner class wrapping just the elevated operation - rather than slapping the keyword on a whole domain or selector class where it would silently apply to every method.

Why this guide differs slightly from the original patterns guidance

Andrew Fawcett's original Apex Enterprise Patterns articles recommended leaving Domain and Selector classes unqualified so they'd implicitly inherit the service's sharing context. inherited sharing didn't exist yet when that was written (it arrived in API 42). This guide has those layers declare inherited sharing explicitly - same intent, but stated in code instead of implied, and it protects you when a domain or selector ends up being some other entry point's first class.

Where else sharing has to be declared

Setting with sharing on your services does not exempt the rest of your org. Every Apex entry point should still declare its sharing mode: controllers, @AuraEnabled classes, Invocable Methods, Batch Apex, Scheduled Apex, REST resources, platform event subscribers. Since most of these delegate straight to your service layer it can feel like you're doubling up - that's no bad thing where security is concerned. And if you expose your service layer as an API for other teams or packages, enforcing the default sharing mode there matters even more, because you can't control what context your callers run in.

The governance stance to adopt org-wide: sharing is on by default, and turning it off requires the developer and a business/solution analyst to justify the specific system-level operation that needs it. It's the same well-known guideline as "always put with sharing on your controllers" - putting it on your service layer just ensures far more than your controller entry points are covered.


Service Layer Code Best Practices

Keeping the code as flexible as possible

You should make sure that the code in the service layer does not expect the data passed to it to be in any particular format. For instance, if the service layer code is expecting a List of Accounts that has a certain set of fields filled out, your service method has just become very fragile. What if the service needs an additional field on that list of accounts to be filled out in the future to do its job? Then you have to refactor all the places building lists of data to send to that service layer method.

Instead you could pass in a set of Account Ids, have the service method query for all the fields it actually requires itself, and then return the appropriate data. This will make your service layer methods much more flexible.

Transaction Management

Your service layer method should handle transaction management (either with the unit of work pattern or otherwise) by making sure to leverage Database.setSavePoint() and using try catch blocks to rollback when the execution fails. If you're wondering why the savepoint is necessary at all when Salesforce "rolls back failed transactions automatically" - it's because a try/catch quietly disables that automatic rollback. Chapter 5 walks through the trap in detail, and it's precisely the boilerplate that commitWork() exists to standardize.

Transaction management example (without the use of a unit of work as discussed in the prior chapter)

public static void calculateOpportunityProfits(Set<Id> accountIdsToCalculate){
List<Account> accountsToCalculate = [SELECT Id FROM Account WHERE Id IN : accountIdsToCalculate];
System.Savepoint savePoint = Database.setSavePoint();
try{
database.insert(accountsToCalculate);
}
catch(Exception e){
Database.rollback(savePoint);
throw e;
}
}

Compound Services

Sometimes code needs to call more than one method in the service layer of your code. In this case instead of calling both service layer methods from your calling code like in the below example, you would ideally want to create a compound service method in your service layer.

Example calling both methods (not ideal)

try{
Account_Service.calculateOpportunityProfits(accountIds);
Account_Service.calculateProjectedOpportunityProfits(accountIds);
}
catch(Exception e){
throw e;
}

The reason the above code is detrimental is that you would either have one of two side effects. The transaction management would only be handled separately by each method and one could fail and the other could complete successfully, despite the fact we don't actually want that to happen. Alternatively you could handle transaction management in the class calling the service layer, which isn't ideal either.

Instead we should create a new method in the service layer that combines those methods and handles the transaction management in a cleaner manner.

Example calling the compound method

try{
Account_Service.calculateRealAndProjectedOpportunityProfits(accountIds);
}
catch(Exception e){
throw e;
}

Why the Service Layer Outlives the UI

If you want the single strongest argument for investing in this layer, here it is: user interfaces and automation tools come and go on this platform, but a well-boundaried service survives all of them, unchanged.

Consider one service method:

public with sharing class Task_Service{
public static void createTasks(Set<Id> recordIds, Schema.SObjectType objectType){
//business logic behind the boundary
}
}

That exact same method is callable, without modification, from every door into your org:

  • An LWC (or Aura component), via a @AuraEnabled wrapper
  • Flow, via a @InvocableMethod wrapper
  • A REST or SOAP API exposure, via a @RestResource wrapper
  • Batch Apex, Queueable, or Scheduled Apex, calling it directly
  • A trigger/domain class that needs the business operation
  • An Agentforce agent action, via that same invocable wrapper

The wrappers are one-liners; the business logic never moves. Now run the platform's history through that lens: the original Apex Enterprise Patterns articles were written when Visualforce was the only UI framework Salesforce had. Aura didn't exist yet. Neither did Lightning Experience, LWC, Flow as we know it, or Agentforce. Every one of those arrived - and Visualforce effectively left - and code organized this way didn't need its business logic rewritten for any of them. Each new front door was just another wrapper. Meanwhile, every org that buried its business logic in Visualforce controller action methods got to rewrite it, once per platform generation.

That's the durability argument in one sentence: you can't predict the next UI framework, but you can make it cost one wrapper class instead of a rewrite.


Implementing the Service Layer

To find out how to implement the Service Layer using the Apex Common Library, continue reading here: Implementing the Service Layer with the Apex Common Library. If you're not interested in utilizing the Apex Common Library, no worries, there are really no frameworks to implement a Service Layer because this is literally just a business logic layer so every single org's service layer will be different. The only thing Apex Common assists with here is abstracting the service layer to assist with Unit Test mocking and to make your service class instantiations more dynamic.


Service Layer Examples

Apex Common Example (Suggested)

All three of the below classes are tied together. We'll go over how this works in the next section.

Task Service Interface

Task Service Class

Task Service Implementation Class