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

  1. No deployments required to change business logic
  2. 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.