A Guide to Writing Cleaner, Modular, and Maintainable Apex Code in Salesforce

A Guide to Writing Cleaner, Modular, and Maintainable Apex Code in Salesforce

In the world of Salesforce development, writing code that works is only half the battle — writing code that is clean, modular, and maintainable is what separates a good developer from a great one.

As your Salesforce org grows, you’ll find that organizing your Apex code into logical layers makes it easier to maintain, extend, and debug. Three key patterns that help achieve this are:
Service Classes
Selector Classes
Constant Classes

Let’s explore how these layers work together to create a robust and scalable architecture.


1. The Need for Modular Apex Code

When we start writing Apex, it’s easy to put all logic inside triggers or controllers. But over time, this leads to:

  • Repeated code across different triggers or classes.

  • Hard-to-read methods that handle too many things.

  • Difficulty in testing and debugging.

A modular approach solves these issues by separating logic into smaller, focused layers — each with a single responsibility.

That’s where the Service, Selector, and Constant classes come in.


2. The Service Class — The “Brain” of the Logic

The Service class acts as the controller or manager of your business logic.
It contains methods that perform actual operations like creating, updating, or processing records.

Think of it as the “brains” behind the feature — it coordinates logic, calls selectors for data, and uses constants for configuration.

🔹 Example:

public with sharing class CaseService {

public static void handleCaseCreation(List<Case> newCases) {
// Fetch data from Selector class
Map<Id, Account> accountMap = CaseSelector.getAccounts(newCases);

// Business logic: update case with account info
for (Case c : newCases) {
if (accountMap.containsKey(c.AccountId)){
c.Description = ‘Account Name: ‘ + accountMap.get(c.AccountId).Name;
} }
update newCases;
} }

Here, the Service class does not directly query data — it delegates that to the Selector class.
This ensures that each layer has a single, clear responsibility.


3. The Selector Class — The “Data Access Layer”

The Selector class is responsible for fetching data from Salesforce.
It contains all your SOQL queries in one place, making it easier to maintain and reuse.

This is often referred to as the data access layer, similar to a “repository” in other programming languages.

🔹 Example:

public with sharing class CaseSelector {
public static Map<Id, Account> getAccounts(List<Case> caseList) {
Set<Id> accountIds = new Set<Id>();
for (Case c : caseList) {
if (c.AccountId != null) {
accountIds.add(c.AccountId);
}
}
return new Map<Id, Account>(
[SELECT Id, Name FROM Account WHERE Id IN :accountIds]
);
}
}

This way, if you ever need to change your SOQL (for example, add more fields or filters), you do it in one place — the Selector class — without touching your business logic.


4. The Constant Class — The “Single Source of Truth”

In every project, you’ll have repeated values like record type names, queue names, status values, etc.
Instead of hardcoding them across multiple classes, you can define them once in a Constant class and reuse them everywhere.

This helps avoid typos, improves readability, and makes updates effortless.

🔹 Example:

public without sharing class CaseConstants {
public static final String RECORDTYPE_CX_OPS = 'CX_Ops';
public static final String RECORDTYPE_CSM_REQUEST = 'CSM_Request';
public static final String QUEUE_CX_OPS = 'CX_Ops_Queue';
}

Now, when your service class needs these values, it simply refers to them like this:

if(caseRecord.RecordTypeId == CaseConstants.RECORDTYPE_CX_OPS) {
// your logic here
}

If something changes — say the record type name — you update it once in the constant class, and it reflects everywhere.


5. How These Layers Work Together

Here’s how these three layers typically interact:

Layer Purpose Example
Service Class Handles business logic and coordination CaseService.handleCaseCreation()
Selector Class Fetches and returns data CaseSelector.getAccounts()
Constant Class Stores reusable values CaseConstants.RECORDTYPE_CX_OPS

When you combine them:

  • Your code becomes reusable across triggers, controllers, and flows.

  • You avoid hardcoding and repetitive logic.

  • You reduce technical debt and improve code readability.


6. Benefits of This Modular Approach

Clean Separation of Logic — Each layer has its own role, making your code easier to understand.
Improved Testability — You can write unit tests for each layer independently.
Scalability — Easier to add new record types or features without breaking existing logic.
Reusability — Shared logic (like selectors or constants) can be used across multiple service classes.
Ease of Maintenance — Change in one layer doesn’t affect others.


7. Best Practices to Follow

  • Always keep SOQL queries in Selector classes.

  • Avoid business logic in triggers — call Service methods instead.

  • Keep constant values in a single Constants class.

  • Follow naming conventions, e.g., CaseService, CaseSelector, CaseConstants.

  • Write unit tests for each layer separately.


Conclusion

Writing modular, clean, and maintainable Apex code isn’t just about good habits — it’s about building a foundation for long-term success.

By structuring your Salesforce code into Service, Selector, and Constant layers, you make it organized, scalable, and easy to maintain — ensuring your org remains efficient as it grows.

Leave a Comment

Your email address will not be published. Required fields are marked *