Extensibility = others can plug in new behaviour without you re-releasing your code. Patterns:
1. Interface + Implementation registry
`apex public interface IDiscountCalculator { Decimal calculate(Opportunity opp); }
public class DiscountFactory { public static IDiscountCalculator getCalculator(String discountType) { Discount_Calculator_Mapping__mdt mapping = Discount_Calculator_Mapping__mdt.getInstance(discountType); return (IDiscountCalculator) Type.forName(mapping.Apex_Class__c).newInstance(); } } `
Custom Metadata maps a discount type to an Apex class name. Adding a new discount type means adding a metadata record + an Apex class implementing IDiscountCalculator — no edits to the factory.
2. Strategy pattern — multiple implementations of the same interface, chosen at runtime.
apex public class DiscountService { private IDiscountCalculator calculator; public DiscountService(IDiscountCalculator calculator) { this.calculator = calculator; } public Decimal applyDiscount(Opportunity opp) { return calculator.calculate(opp); } }
The caller injects the right strategy.
3. Hooks / callbacks — let extending code register handlers to run at specific points.
`apex public class OrderProcessor { private static List<IOrderHook> hooks = new List<IOrderHook>(); public static void registerHook(IOrderHook hook) { hooks.add(hook); }
public void processOrder(Order o) { for (IOrderHook hook : hooks) hook.beforeProcess(o); // core processing for (IOrderHook hook : hooks) hook.afterProcess(o); } } `
4. Plugin pattern — load implementations dynamically via Custom Metadata listing class names.
apex public class PluginManager { public static List<IPlugin> loadPlugins(String pluginType) { List<IPlugin> plugins = new List<IPlugin>(); for (Plugin__mdt pl : [SELECT Apex_Class__c FROM Plugin__mdt WHERE Type__c = :pluginType AND Active__c = true ORDER BY Order__c]) { plugins.add((IPlugin) Type.forName(pl.Apex_Class__c).newInstance()); } return plugins; } }
5. Configurable behaviour via Custom Metadata — for simpler cases, expose configuration knobs rather than full extensibility.
apex Order_Config__mdt cfg = Order_Config__mdt.getInstance('default'); if (cfg.AutoApprove_Discounts__c) { ... }
6. `global virtual` classes — within a managed package, declare extensibility points as global virtual. Subscribers can subclass and override.
apex global virtual class StandardDiscountCalculator implements IDiscountCalculator { global virtual Decimal calculate(Opportunity opp) { return ...; } } // Subscriber: class CustomDiscountCalculator extends StandardDiscountCalculator { override calculate(...) }
Trade-offs:
- More extensibility = more complexity. Don't over-engineer for hypothetical needs.
- Versioning — interface changes break extending code. Once published, interfaces should be stable.
- Testing — extension points need tests proving registered handlers actually run.
- Performance — dynamic dispatch is slower than direct calls.
When to use which:
- Configurable behaviour -> Custom Metadata flags.
- Multiple implementations chosen at runtime -> Interface + Factory.
- Pluggable extensions added by other modules -> Plugin pattern.
- ISV package allowing customer customisation ->
global virtual+ interfaces.
A well-designed extensible Apex framework is what enables managed packages to remain useful across customer-specific customisations — without the customer having to fork the package.
