Why design patterns matter in Apex
Apex is Java-like but constrained by governor limits (100 SOQL queries, 150 DML statements, 10s CPU time per transaction). Design patterns help you stay within these limits while keeping code maintainable. These 7 patterns solve 90% of the challenges Salesforce developers face: bulkification, sharing data across triggers, lazy loading, strategy selection, and error handling.
Pattern 1: Singleton
Ensures a class has only one instance, with a global access point. Use case: loading configuration/custom metadata once and reusing it across the transaction. Implementation: a static variable holds the instance; a static getInstance() method creates it on first access. This prevents 100 SOQL queries for the same config metadata in a bulk trigger.
Pattern 2: Bulkification (Batch Pattern)
The most important Apex pattern. Never do SOQL or DML inside a loop. Instead: collect all IDs in a Set, run one SOQL to fetch all related records, process in memory, then do one DML on a List. This is the difference between a trigger that works on 200 records and one that hits the 100 SOQL limit on record 101.
Pattern 3: Trigger Handler / Dispatcher
Separate trigger logic from the trigger itself. The trigger delegates to a handler class based on context (before insert, after update). This keeps triggers clean (one line per context) and makes logic testable. Use a TriggerHandler base class with virtual methods, extended by object-specific handlers.
Pattern 4: Strategy
Define a family of algorithms, encapsulate each, and make them interchangeable. Use case: discount calculation with different rules for different customer types. Implementation: a DiscountStrategy interface, concrete classes (VolumeDiscount, LoyaltyDiscount, PromoDiscount), and a context class that selects the strategy at runtime. This avoids massive if-else chains.
Pattern 5: Factory
Create objects without specifying the exact class. Use case: creating different record types (Lead vs Person Account) based on input data. Implementation: a factory class with a createRecord(input) method that returns the right SObject subclass. This centralizes object creation logic and makes it easy to add new types.
Pattern 6: Chain of Responsibility
Pass a request along a chain of handlers, each deciding to process or pass along. Use case: validation pipeline (validate required fields โ validate business rules โ validate permissions). Implementation: a chain of handler classes, each with a setNext() and handle() method. If a handler can't process, it passes to the next. This decouples validation logic.
Pattern 7: Async (Queueable / Batch)
For long-running operations (>10s CPU or >100 callouts), use async Apex. Queueable for single-job async work (callouts, complex processing). Batch for bulk processing (>10k records). Future for fire-and-forget. Pattern: a job class implementing Queueable/Batchable, dispatched from a trigger/handler with System.enqueueJob() or Database.executeBatch(). Always include a stateful error handler.
"Bulkification is the difference between a trigger that works on 200 records and one that hits the 100 SOQL limit on record 101."
Key Takeaway
Master these 7: Singleton (config caching), Bulkification (no SOQL/DML in loops), Trigger Handler (clean triggers), Strategy (algorithm selection), Factory (object creation), Chain of Responsibility (validation pipelines), and Async (Queueable/Batch for long-running work).