Enterprise Design Patterns in Apex
📖 Concept
Design patterns in Salesforce go beyond academic theory — they solve real problems of code organization, testability, and maintainability in enterprise orgs with millions of records and dozens of developers.
The Apex Enterprise Patterns (Andrew Fawcett's framework):
Service Layer — Business logic entry point
- Contains use cases and orchestration logic
- Called by triggers, LWC, REST APIs, Batch
- Manages transactions and error handling
- Example:
OpportunityService.closeDeals(oppIds)
Domain Layer — Object-specific business rules
- Encapsulates validation, defaults, calculations per SObject
- Triggered by DML operations
- Contains before/after trigger logic
- Example:
Opportunities.validate(records)
Selector Layer — Query encapsulation
- All SOQL queries for an object in one class
- Enforces security (with sharing, FLS)
- Cacheable, testable, reusable
- Example:
AccountsSelector.getByIds(ids)
Unit of Work — Transaction management
- Collects all DML operations and executes in optimal order
- Handles parent-child insert ordering
- Single commit point for transaction
- Example:
uow.registerNew(account); uow.commitWork();
SOLID Principles in Apex:
S — Single Responsibility: Each class does one thing
O — Open/Closed: Extend via inheritance, don't modify existing code
L — Liskov Substitution: Subtypes must be substitutable for base types
I — Interface Segregation: Small, focused interfaces
D — Dependency Inversion: Depend on abstractions, not implementations
Common Salesforce Design Patterns:
- Strategy Pattern — Swap algorithms at runtime (different pricing strategies)
- Factory Pattern — Create objects without specifying exact class
- Facade Pattern — Simplified interface to complex subsystems
- Observer Pattern — Platform Events / trigger-based notifications
- Singleton Pattern — Single instance per transaction (static variables)
💻 Code Example
1// Enterprise Design Patterns Implementation23// 1. SERVICE LAYER — Business logic orchestration4public with sharing class OpportunityService {56 // Public API — called by triggers, LWC, REST, Batch7 public static void closeDeals(Set<Id> opportunityIds) {8 // Query via Selector9 List<Opportunity> opps = OpportunitiesSelector.getByIds(opportunityIds);1011 // Apply domain logic12 Opportunities domain = new Opportunities(opps);13 domain.validateForClose();14 domain.calculateFinalAmounts();1516 // Register changes via Unit of Work17 fflib_ISObjectUnitOfWork uow = Application.UnitOfWork.newInstance();1819 for (Opportunity opp : opps) {20 opp.StageName = 'Closed Won';21 opp.CloseDate = Date.today();22 uow.registerDirty(opp);2324 // Create follow-up task25 Task followUp = new Task(26 Subject = 'Post-close follow-up: ' + opp.Name,27 WhatId = opp.Id,28 OwnerId = opp.OwnerId,29 ActivityDate = Date.today().addDays(7)30 );31 uow.registerNew(followUp);32 }3334 uow.commitWork(); // Single DML commit35 }3637 public static void applyDiscount(Set<Id> oppIds, Decimal discountPercent) {38 List<Opportunity> opps = OpportunitiesSelector.getByIds(oppIds);39 Opportunities domain = new Opportunities(opps);40 domain.applyDiscount(discountPercent);41 update opps;42 }43}4445// 2. DOMAIN LAYER — Object-specific business rules46public class Opportunities {47 private List<Opportunity> records;4849 public Opportunities(List<Opportunity> records) {50 this.records = records;51 }5253 // Validation54 public void validateForClose() {55 for (Opportunity opp : records) {56 if (opp.Amount == null || opp.Amount <= 0) {57 opp.addError('Amount must be positive to close');58 }59 if (opp.ContactId == null) {60 opp.addError('Primary Contact required to close');61 }62 }63 }6465 // Business calculation66 public void calculateFinalAmounts() {67 for (Opportunity opp : records) {68 if (opp.Discount_Percent__c != null) {69 opp.Amount = opp.Amount * (1 - opp.Discount_Percent__c / 100);70 }71 }72 }7374 // Apply discount75 public void applyDiscount(Decimal percent) {76 for (Opportunity opp : records) {77 opp.Discount_Percent__c = percent;78 opp.Amount = opp.Amount * (1 - percent / 100);79 }80 }81}8283// 3. SELECTOR LAYER — Query encapsulation84public inherited sharing class OpportunitiesSelector {8586 private static final List<String> DEFAULT_FIELDS = new List<String>{87 'Id', 'Name', 'StageName', 'Amount', 'CloseDate',88 'AccountId', 'OwnerId', 'ContactId', 'Discount_Percent__c'89 };9091 public static List<Opportunity> getByIds(Set<Id> ids) {92 return Database.query(93 'SELECT ' + String.join(DEFAULT_FIELDS, ', ') +94 ' FROM Opportunity WHERE Id IN :ids'95 );96 }9798 public static List<Opportunity> getByAccountIds(Set<Id> accountIds) {99 return Database.query(100 'SELECT ' + String.join(DEFAULT_FIELDS, ', ') +101 ' FROM Opportunity WHERE AccountId IN :accountIds' +102 ' ORDER BY CloseDate DESC'103 );104 }105106 public static List<Opportunity> getOpenByOwner(Id ownerId) {107 return Database.query(108 'SELECT ' + String.join(DEFAULT_FIELDS, ', ') +109 ' FROM Opportunity WHERE OwnerId = :ownerId' +110 ' AND IsClosed = false ORDER BY CloseDate ASC'111 );112 }113}114115// 4. STRATEGY PATTERN — Swappable business logic116public interface IPricingStrategy {117 Decimal calculatePrice(Opportunity opp);118}119120public class StandardPricing implements IPricingStrategy {121 public Decimal calculatePrice(Opportunity opp) {122 return opp.Amount; // No discount123 }124}125126public class EnterprisePricing implements IPricingStrategy {127 public Decimal calculatePrice(Opportunity opp) {128 Decimal discount = opp.Amount > 100000 ? 0.15 : 0.05;129 return opp.Amount * (1 - discount);130 }131}132133public class PartnerPricing implements IPricingStrategy {134 public Decimal calculatePrice(Opportunity opp) {135 return opp.Amount * 0.70; // 30% partner discount136 }137}138139// Usage140public class PricingService {141 public static Decimal getPrice(Opportunity opp, String channel) {142 IPricingStrategy strategy;143144 switch on channel {145 when 'Standard' { strategy = new StandardPricing(); }146 when 'Enterprise' { strategy = new EnterprisePricing(); }147 when 'Partner' { strategy = new PartnerPricing(); }148 when else { strategy = new StandardPricing(); }149 }150151 return strategy.calculatePrice(opp);152 }153}154155// 5. FACTORY PATTERN — Dynamic handler creation156public class TriggerHandlerFactory {157158 private static Map<Schema.SObjectType, Type> handlerRegistry =159 new Map<Schema.SObjectType, Type>{160 Account.SObjectType => AccountTriggerHandler.class,161 Contact.SObjectType => ContactTriggerHandler.class,162 Opportunity.SObjectType => OpportunityTriggerHandler.class163 };164165 public static ITriggerHandler getHandler(Schema.SObjectType objType) {166 Type handlerType = handlerRegistry.get(objType);167 if (handlerType == null) {168 throw new TriggerException('No handler registered for: ' + objType);169 }170 return (ITriggerHandler) handlerType.newInstance();171 }172}
🏋️ Practice Exercise
Design Pattern Practice:
- Implement a Service-Domain-Selector architecture for the Account object
- Build a Strategy pattern for different notification channels (Email, SMS, Slack)
- Create a Factory that dynamically instantiates trigger handlers based on SObject type
- Implement the Unit of Work pattern for a transaction inserting parent and child records
- Apply SOLID principles to refactor a 500-line trigger handler into separate classes
- Build a Facade that simplifies a complex integration with 5 external services
- Implement dependency injection in Apex using interfaces and factory methods
- Create a Singleton pattern for a configuration cache that persists throughout a transaction
- Build a Domain-Driven Design model for a healthcare patient management system
- Code review a colleague's implementation and recommend design pattern improvements
⚠️ Common Mistakes
Over-engineering simple code — not every class needs 5 layers. Use patterns when complexity warrants them
Mixing query logic into service/domain classes — all SOQL should be in Selectors for reusability and security enforcement
Not using interfaces for dependency injection — concrete class dependencies make testing difficult. Always depend on abstractions
Ignoring Apex's limitations when applying Java patterns — Apex lacks generics, has limited reflection, and has governor limits. Adapt patterns accordingly
Creating too many small classes — the Apex class limit per org is ~6,000. Balance class count with maintainability
💼 Interview Questions
🎤 Mock Interview
Mock interview is powered by AI for Enterprise Design Patterns in Apex. Login to unlock this feature.