Apex Design Patterns , Difference between Factory pattern and Strategy pattern
When you’re just getting started in Apex, writing code that works feels like winning. But as your org grows (and so does your team), that “working code” starts to smell like… technical debt. 😬
That’s where Apex Design Patterns come in. They’re not just fancy words — they’re proven structures that help you write clean, scalable, and maintainable code.
Let’s unpack the most useful Apex design patterns, one by one, with real-world examples from projects we’ve all bumped into.
1️⃣ The Trigger Handler Pattern (a.k.a. Stop Writing Logic in Triggers!)
The Problem:
You’ve written all your logic inside the trigger body. Now it’s bloated, untestable, and every new feature breaks something else.
The Pattern:
Move your logic to a separate class — usually called TriggerHandler
— and keep the trigger skinny.
Example:
trigger ContactTrigger on Contact (before insert, before update) {
ContactTriggerHandler handler = new ContactTriggerHandler();
if(Trigger.isBefore && Trigger.isInsert){
handler.beforeInsert(Trigger.new);
}
}
public class ContactTriggerHandler {
public void beforeInsert(List<Contact> newContacts) {
for(Contact c : newContacts){
if(String.isBlank(c.LastName)){
c.LastName = 'Default';
}
}
}
}
Real-Life Use:
Any org where Contacts or Opportunities have complex rules. Use this and your future team will thank you.
2️⃣ The Service Layer Pattern (Organize Business Logic)
The Problem:
Logic is scattered across controllers, triggers, batch classes, etc.
The Pattern:
Centralize your core logic in a dedicated Service Class (like AccountService
or OpportunityService
).
Example:
public class OpportunityService {
public static void assignSalesManager(List<Opportunity> opps) {
for(Opportunity opp : opps){
opp.OwnerId = '005xx000001XYZ';
}
}
}
Now your Trigger, LWC controller, or Flow can all call this shared logic.
P.S.: Do not hardcode the IDs in Apex. Just like I did above.
Real-Life Use:
Useful when multiple entry points (Triggers, Apex, Flow Actions) need the same processing logic.
3️⃣ The Factory Pattern (Object Creation Wizardry)
The Problem:
You need to instantiate different classes based on user input or metadata.
The Pattern:
Use a Factory class that abstracts away the creation logic.
Example:
public interface NotificationStrategy {
void send(String message);
}
public class EmailStrategy implements NotificationStrategy {
public void send(String message) {
// Send email
}
}
public class SmsStrategy implements NotificationStrategy {
public void send(String message) {
// Send SMS
}
}
public class NotificationFactory {
public static NotificationStrategy getStrategy(String type) {
if(type == 'Email') return new EmailStrategy();
if(type == 'SMS') return new SmsStrategy();
return null;
}
}
Real-Life Use:
Dynamic notifications, record processing logic, or integrations.
4️⃣ The Strategy Pattern (Change Behavior Without Changing Code)
The Problem:
Your boss wants different discount rules for different customer types.
So what do you do?
- Copy-paste the discount logic into 3 different classes? 😵
- Add 12 nested
if-else
blocks and pray it doesn’t explode later? 🤕
The Pattern:
Use separate classes for each “strategy” (Retail, Wholesale, VIP), and dynamically switch them using a common interface.
👨💻 Example: Discount Strategy for Customer Types
Let’s say we have 3 customer types: Retail
, Wholesale
, and VIP
. Each gets a different discount logic.
Step 1: Define the Strategy Interface
public interface DiscountStrategy {
Decimal calculateDiscount(Decimal originalPrice);
}
Step 2: Implement the Strategies
public class RetailDiscount implements DiscountStrategy {
public Decimal calculateDiscount(Decimal originalPrice) {
return originalPrice * 0.95; // 5% discount
}
}
public class WholesaleDiscount implements DiscountStrategy {
public Decimal calculateDiscount(Decimal originalPrice) {
return originalPrice * 0.90; // 10% discount
}
}
public class VIPDiscount implements DiscountStrategy {
public Decimal calculateDiscount(Decimal originalPrice) {
return originalPrice * 0.80; // 20% discount
}
}
Step 3: Use a Factory to Select the Strategy
public class DiscountStrategyFactory {
public static DiscountStrategy getStrategy(String customerType) {
if(customerType == 'Retail') return new RetailDiscount();
if(customerType == 'Wholesale') return new WholesaleDiscount();
if(customerType == 'VIP') return new VIPDiscount();
// Default fallback
return new RetailDiscount();
}
}
Step 4: Use in Your Code Like a Pro
public class DiscountService {
public static Decimal applyDiscount(String customerType, Decimal price) {
DiscountStrategy strategy = DiscountStrategyFactory.getStrategy(customerType);
return strategy.calculateDiscount(price);
}
}
Real-Life Usage:
You could use this in:
- Product pricing rules in an eCommerce app
- Commission calculation logic
- Shipping charges for different zones
Test It:
Decimal finalPrice = DiscountService.applyDiscount('VIP', 1000);
System.debug('Discounted Price: ' + finalPrice); // should return 800
🎯 Now you’ve got flexible, readable, and testable code — and when business asks for a new “Startup” discount type, you won’t cry 😅.
5️⃣ The Singleton Pattern (One Instance to Rule Them All)
The Problem:
You need a central utility or config class, but don’t want it recreated each time.
The Pattern:
Ensure a class has only one instance and provide a global point of access.
Example:
public class AppSettings {
private static AppSettings instance;
public String defaultCountry { get; private set; }
private AppSettings() {
defaultCountry = 'India';
}
public static AppSettings getInstance() {
if(instance == null){
instance = new AppSettings();
}
return instance;
}
}
Real-Life Use:
Global settings, reusable utilities, or app-wide configuration.
6️⃣ The Unit of Work Pattern (Bulk DML, Elegantly)
The Problem:
You’re performing multiple DMLs across related records in different service methods. It’s hard to manage and easy to hit governor limits.
The Pattern:
Accumulate all records and commit in one go.
Example:
Use patterns like Fflib‘s UnitOfWork
or your own to track inserts/updates/deletes and execute in one place.
Bonus: Don’t Use Patterns Just to Sound Smart
Design patterns are tools — not rules. Don’t shove a Factory pattern where a single method will do. 😄
Use them when:
- You need scalability
- You want to avoid duplicate logic
- Your org is growing or has multiple devs
- You’re preparing for enterprise-level architecture
👨💻 Real Life Checklist: When to Use Which
Scenario | Recommended Pattern |
---|---|
Trigger becoming unmanageable | Trigger Handler |
Logic reused across classes | Service Layer |
Need different execution paths | Strategy or Factory |
Need one config holder | Singleton |
Managing complex DML | Unit of Work |
🤔 “Wait, aren’t Strategy and Factory the same thing?”
Not quite. Think of it like this:
Feature | Strategy Pattern | Factory Pattern |
---|---|---|
🎯 Goal | Choose behavior at runtime | Create object at runtime |
🧠 Focus | How an object behaves (business logic) | What object to instantiate |
🧩 Structure | Interface + multiple interchangeable behaviors | Interface + central creator method |
🔁 Flexibility | Swap logic without modifying consumer code | Add new object types without changing code |
🛠 Common Use Case | Discount rules, tax logic, validation styles | Instantiating different notification types, services |
🔄 Can they work together? | Yes! Factory can create the right strategy | Often combined with Strategy Pattern |
🍿 Real World Analogy:
- Strategy Pattern is like choosing a payment method at checkout: Credit Card, PayPal, UPI, etc. The action (paying) stays the same, but the internal logic differs.
- Factory Pattern is like ordering food from Swiggy. You choose a cuisine (Italian, Indian, Chinese), and the system instantiates the right food object behind the scenes.
🤹♂️ In Apex Terms:
✅ Strategy Example:
interface TaxCalculator { Decimal calculate(Decimal amount); }
class IndiaGST implements TaxCalculator { ... }
class USVAT implements TaxCalculator { ... }
TaxCalculator calculator = new IndiaGST();
calculator.calculate(100);
You are swapping logic — the tax calculation — without changing the consumer code.
✅ Factory Example:
interface Notification { void send(String message); }
class EmailNotification implements Notification { ... }
class SMSNotification implements Notification { ... }
Notification notify = NotificationFactory.getNotification('Email');
notify.send('Hello World');
You are dynamically creating the right object based on input.
🧬 Combine Them:
Often, you’ll use a Factory to return the right Strategy:
DiscountStrategy strategy = DiscountStrategyFactory.getStrategy(customerType);
Decimal finalPrice = strategy.calculateDiscount(price);
👆 See? The Factory chose which logic to use, and Strategy defined how the logic behaves.
(Too Long; Didn’t Read):
- Use Strategy when:
➤ You want to switch between algorithms or logic without bloating your code. - Use Factory when:
➤ You want to abstract object creation and delegate the “which class” decision to a central place.
Apex Design Patterns
Strategy pattern Salesforce example, Strategy pattern Salesforce example, Strategy pattern Salesforce example
Factory pattern in Apex,Factory pattern in Apex,Factory pattern in Apex
Read more