Apex Design Patterns Every Salesforce Developer Should Know (with Real-Life Examples)

Apex Design Patterns
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

ScenarioRecommended Pattern
Trigger becoming unmanageableTrigger Handler
Logic reused across classesService Layer
Need different execution pathsStrategy or Factory
Need one config holderSingleton
Managing complex DMLUnit of Work

🤔 “Wait, aren’t Strategy and Factory the same thing?”

Not quite. Think of it like this:


FeatureStrategy PatternFactory Pattern
🎯 GoalChoose behavior at runtimeCreate object at runtime
🧠 FocusHow an object behaves (business logic)What object to instantiate
🧩 StructureInterface + multiple interchangeable behaviorsInterface + central creator method
🔁 FlexibilitySwap logic without modifying consumer codeAdd new object types without changing code
🛠 Common Use CaseDiscount rules, tax logic, validation stylesInstantiating different notification types, services
🔄 Can they work together?Yes! Factory can create the right strategyOften 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

Exit mobile version