Designing for Extensibility in Salesforce: Architecting for Change
In Salesforce, change is constant — new features, new business processes, new users. If your solution can’t adapt without major rewrites, you’re building tech debt.
This is where extensibility comes in. Extensibility means designing your Salesforce apps so they can evolve easily, with minimal code changes.
Let’s look at real-world strategies that architects use to design for flexibility and long-term success.
1. Use Custom Metadata and Custom Settings — Not Hardcoded Values
Bad Example
if(account.Type == 'Customer - Direct') {
// do something
}
Better Approach
- Store the type in Custom Metadata:
- Create a Custom Metadata Type: Account_Type_Mapping
- Add a record: Label = Direct Customer, DeveloperName = Customer_Direct
- Reference it in code:
if(account.Type == Account_Type_Mapping__mdt.getInstance('Customer_Direct').Value__c) {
// do something
}
Benefits
- No deployments required to change business logic
- Admins can manage config without touching code
2. Build with Dynamic Apex Where It Makes Sense
Dynamic Apex allows your code to respond to metadata changes.
Example: Generic Field Copy
SObject source = ...;
SObject target = ...;
for(Schema.SObjectField f : source.getSObjectType().getDescribe().fields.getMap().values()) {
String fieldName = f.getDescribe().getName();
if(source.get(fieldName) != null)
target.put(fieldName, source.get(fieldName));
}
3. Use Interface-Based Apex Design
Interfaces allow plug-and-play logic — making your code easy to extend without touching the core.
Step 1: Define the Interface
public interface DiscountStrategy {
Decimal applyDiscount(Decimal amount);
}
Step 2: Create Multiple Implementations
public class PercentageDiscount implements DiscountStrategy {
public Decimal applyDiscount(Decimal amount) {
return amount * 0.9; // 10% off
}
}
public class FlatAmountDiscount implements DiscountStrategy {
public Decimal applyDiscount(Decimal amount) {
return amount - 20; // Flat ₹20 discount
}
}
Step 3: Use It Dynamically
DiscountStrategy strategy;
if(userType == 'Premium') {
strategy = new PercentageDiscount();
} else {
strategy = new FlatAmountDiscount();
}
Decimal discounted = strategy.applyDiscount(200);
System.debug('Final Price: ' + discounted);
4. Design Flows and Apex to Be Triggered by Metadata
Use Custom Metadata + Flows + Invocable Apex to dynamically drive automation.
Create a Flow_Config__mdt metadata type to control:
- When a flow runs
- With what parameters
- Under which conditions
This allows you to enable or disable logic without duplicating flows or deploying new versions.
5. Avoid Tight Coupling: Keep Your Apex Loose and Modular
Avoid monolithic triggers or massive utility classes. Break your logic into layers:
- Trigger → Trigger Handler → Domain Layer → Service Layer
Each layer should do one job and be independently testable.
Example: Breaking Down Logic into Layers
Trigger
trigger AccountTrigger on Account (before insert, after insert) {
if(Trigger.isBefore) {
AccountTriggerHandler.beforeInsert(Trigger.new);
}
if(Trigger.isAfter) {
AccountTriggerHandler.afterInsert(Trigger.new);
}
}
Trigger Handler
public class AccountTriggerHandler {
public static void beforeInsert(List<Account> accounts) {
// Business logic for 'before insert'
for(Account acc : accounts) {
if(acc.Name == null) {
acc.Name = 'Default Account Name';
}
}
}
public static void afterInsert(List<Account> accounts) {
// Post-insert logic, like sending notifications
NotificationService.sendNewAccountNotification(accounts);
}
}
Service Handler
public class NotificationService {
public static void sendNewAccountNotification(List<Account> accounts) {
// Send email notification for new account creation
for(Account acc : accounts) {
if(acc.OwnerId != null) {
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
mail.setSubject('New Account Created');
mail.setToAddresses(new String[] {'example@example.com'});
mail.setPlainTextBody('A new account has been created: ' + acc.Name);
Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
}
}
}
}
Why this helps:
- Improves testability
- Encourages separation of concerns
- Makes logic reusable and less fragile
6. Use Platform Events or Custom Notifications Instead of Direct Calls
Loose coupling improves scalability. Instead of calling code directly, publish an event.
Example:
My_Event__e event = new My_Event__e(Field__c = 'Value');
Database.SaveResult sr = EventBus.publish(event);
Subscribe to the event using Process Builder, Flow, or Apex Trigger. This design makes your system modular and easy to expand with new listeners without touching existing code.