With the increasing complexity of Salesforce application development, the backend logic developed in Apex is frequently hard to manage, test and scale. A lot of the development begins with simple triggers and helper classes but as time passes this makes the development of code that is tightly coupled together and results in what is referred to as "spaghetti."
The Services Layer Architecture is considered one of many proven best practices when developing scalable and maintainable systems using Apex.
What is the Apex Services Layer?
The Services Layer is a design pattern that helps you separate your business logic from your triggers, controllers, and database operations. With the Apex Services Layer you will create service classes that centralize your logic instead of embedding that logic directly in your triggers and Lightning controllers.
A well-designed Apex Services Layer will provide:
- A clean separation of concerns
- The ability to reuse logic
- Easier testing and debugging
- Improved scalability of enterprise systems
Preferred Layered Structure For A Scalable Salesforce Backend
Standard practice for a scalable Salesforce backend is to have the following layers:
- Trigger Layer - Contains minimal business logic and delegates execution to handlers.
- Handler Layer - Coordinates the execution of business logic in the overall system.
- Service Layer - Has the majority of the business logic.
- Domain Layer - (optional) Contains object-level business logic.
- Selector Layer - Contains all SOQL queries.
The layered structure is commonly used in backend development across all industries, but also at companies such as Cleveroad that provide a focus on developing maintainable backends that are extensible.
With one single function for each layer, the entire backlog becomes more modular and simple to grow.
Step 1: Keep Triggers Thin
Triggers should only act as entry points and delegate logic:
trigger AccountTrigger on Account (before insert, before update) {
AccountTriggerHandler.handleBeforeInsert(Trigger.new);
}
Avoid putting business logic directly inside triggers — this is one of the most common anti-patterns.
Step 2: Use a Handler Layer
The handler layer controls execution flow and calls services:
public class AccountTriggerHandler {
public static void handleBeforeInsert(List<Account> accounts) {
AccountService.validateAccounts(accounts);
}
}
This keeps triggers clean and allows centralized control over logic execution.
Step 3: Implement the Service Layer
The Service Layer is where your core business logic lives:
public class AccountService {
public static void validateAccounts(List<Account> accounts) {
for (Account acc : accounts) {
if (acc.AnnualRevenue == null) {
acc.addError('Annual Revenue is required.');
}
}
}
}
This layer needs to:
- Can be used again and again for triggers, APIs, and batch jobs
- Have only business logic
- When you can, don't use direct SOQL (delegate to selectors)
Step 4: Add a Selector Layer for Queries
To avoid scattered SOQL queries, use a selector pattern:
public class AccountSelector {
public static List<Account> getByIds(Set<Id> accountIds) {
return [SELECT Id, Name, AnnualRevenue FROM Account WHERE Id IN :accountIds];
}
}
Benefits:
- Centralized query logic
- Easier optimization for large data volumes
- Better maintainability
Step 5: Make It Testable
One of the best things about the Services Layer is that it makes testing easier.
You can test services on their own instead of testing triggers directly:
@isTest
private class AccountServiceTest {
@isTest
static void testValidation() {
Account acc = new Account(Name = 'Test');
List<Account> accounts = new List<Account>{acc};
Test.startTest();
AccountService.validateAccounts(accounts);
Test.stopTest();
System.assert(acc.getErrors().size() > 0);
}
}
This makes your tests:
- Faster
- More focused
- Easier to maintain
Best Practices for Scalable Backend Design
If you're creating a Salesforce Services Layer, adhere to these principles:
One: Business Logic Should Not Be in Trigger(s): Triggers should only delegate to (have no business logic or process being completed in) the Services Layer.
Two: Design Methods for Bulk Operations: All service methods must accept a collection/list of records instead of an individual record.
Three: Use Asynchronous Processing: When processing heavy loads, utilize one or more of these methods:
- Queueable Apex
- Batch Apex
- Future Methods
Four: Use Separate Logic for Read and Write Actions: Create a read-only selector class and use the services layer to perform writes.
Five: Provide Detailed Error Handling: Create an error log and provide meaningful error messages.
When a company adopts a multi-layered architecture for its software back end, it is not just a way to organize its code. It also reflects a company's overall approach to engineering. Many software development businesses like Cleveroad, which focus on providing back end development services, have a strong emphasis on software design with proper structures. This means that companies will build their CRM systems (for example, Salesforce) to grow and change as the company's business evolves without creating technical debt along the way.
Common Errors to Prevent
- Combining SOQL Investigations with Service Calls
- Creating one Service class with multiple responsibilities
- Not Ensuring Bulk Processing
- Lacking a Complete Set of Tests
Conclusion
When creating a scalable back end in Salesforce, it is necessary to do more than simply write working code in apex. By creating a services layer application you have developed a modular, programmable, and scalable application to support the changing needs of your business.
This will not only improve the overall quality of your application but will also allow for better collaboration between teams and faster delivery of new functionality.
As a developer creating high-level corporate applications for Salesforce, you must use a services layer application; failing to do so will hinder your ability to create high-quality scalable applications.
Leave a Comment