Enterprise Design Patterns in Apex

0/2 in this phase0/41 across the roadmap

📖 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):

  1. 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)
  2. 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)
  3. 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)
  4. 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

codeTap to expand ⛶
1// Enterprise Design Patterns Implementation
2
3// 1. SERVICE LAYER — Business logic orchestration
4public with sharing class OpportunityService {
5
6 // Public API — called by triggers, LWC, REST, Batch
7 public static void closeDeals(Set<Id> opportunityIds) {
8 // Query via Selector
9 List<Opportunity> opps = OpportunitiesSelector.getByIds(opportunityIds);
10
11 // Apply domain logic
12 Opportunities domain = new Opportunities(opps);
13 domain.validateForClose();
14 domain.calculateFinalAmounts();
15
16 // Register changes via Unit of Work
17 fflib_ISObjectUnitOfWork uow = Application.UnitOfWork.newInstance();
18
19 for (Opportunity opp : opps) {
20 opp.StageName = 'Closed Won';
21 opp.CloseDate = Date.today();
22 uow.registerDirty(opp);
23
24 // Create follow-up task
25 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 }
33
34 uow.commitWork(); // Single DML commit
35 }
36
37 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}
44
45// 2. DOMAIN LAYER — Object-specific business rules
46public class Opportunities {
47 private List<Opportunity> records;
48
49 public Opportunities(List<Opportunity> records) {
50 this.records = records;
51 }
52
53 // Validation
54 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 }
64
65 // Business calculation
66 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 }
73
74 // Apply discount
75 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}
82
83// 3. SELECTOR LAYER — Query encapsulation
84public inherited sharing class OpportunitiesSelector {
85
86 private static final List<String> DEFAULT_FIELDS = new List<String>{
87 'Id', 'Name', 'StageName', 'Amount', 'CloseDate',
88 'AccountId', 'OwnerId', 'ContactId', 'Discount_Percent__c'
89 };
90
91 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 }
97
98 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 }
105
106 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}
114
115// 4. STRATEGY PATTERN — Swappable business logic
116public interface IPricingStrategy {
117 Decimal calculatePrice(Opportunity opp);
118}
119
120public class StandardPricing implements IPricingStrategy {
121 public Decimal calculatePrice(Opportunity opp) {
122 return opp.Amount; // No discount
123 }
124}
125
126public 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}
132
133public class PartnerPricing implements IPricingStrategy {
134 public Decimal calculatePrice(Opportunity opp) {
135 return opp.Amount * 0.70; // 30% partner discount
136 }
137}
138
139// Usage
140public class PricingService {
141 public static Decimal getPrice(Opportunity opp, String channel) {
142 IPricingStrategy strategy;
143
144 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 }
150
151 return strategy.calculatePrice(opp);
152 }
153}
154
155// 5. FACTORY PATTERN — Dynamic handler creation
156public class TriggerHandlerFactory {
157
158 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.class
163 };
164
165 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:

  1. Implement a Service-Domain-Selector architecture for the Account object
  2. Build a Strategy pattern for different notification channels (Email, SMS, Slack)
  3. Create a Factory that dynamically instantiates trigger handlers based on SObject type
  4. Implement the Unit of Work pattern for a transaction inserting parent and child records
  5. Apply SOLID principles to refactor a 500-line trigger handler into separate classes
  6. Build a Facade that simplifies a complex integration with 5 external services
  7. Implement dependency injection in Apex using interfaces and factory methods
  8. Create a Singleton pattern for a configuration cache that persists throughout a transaction
  9. Build a Domain-Driven Design model for a healthcare patient management system
  10. 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.