In Salesforce development, the ability to build flexible, maintainable, and scalable applications relies heavily on implementing object-oriented programming principles effectively.
Two critical concepts that facilitate this are virtual/abstract classes and interfaces.
In this blog post, we’ll explore these concepts in depth and see how they can be applied in Salesforce Apex to create robust solutions.
Virtual Classes in Salesforce
Virtual classes in Apex serve as foundational templates that other classes can extend. They allow you to define methods that subclasses can override, providing a way to implement common functionality while enabling customization.
Key Characteristics of Virtual Classes
- Declared using the
virtualkeyword - Can contain implemented methods and properties
- Can be instantiated directly (unlike abstract classes)
- Can have methods marked as
virtualthat subclasses can override
Virtual Class Example
public virtual class PaymentProcessor {
// Regular method that will be inherited as-is
public Decimal calculateTax(Decimal amount) {
return amount * 0.1; // 10% tax
}
// Virtual method that can be overridden by subclasses
public virtual Decimal processPayment(Decimal amount) {
// Default implementation
System.debug('Processing standard payment of $' + amount);
return amount;
}
// Another virtual method
public virtual Boolean validatePayment(Decimal amount) {
return amount > 0;
}
}
Extending a Virtual Class
public class CreditCardProcessor extends PaymentProcessor {
// Override the virtual method with custom implementation
public override Decimal processPayment(Decimal amount) {
// Custom implementation for credit card processing
System.debug('Processing credit card payment of $' + amount);
Decimal processingFee = amount * 0.03; // 3% fee
return amount - processingFee;
}
}
In this example, CreditCardProcessor inherits the calculateTax method as-is but customizes the payment processing logic with its own implementation.


Abstract Classes in Salesforce
Abstract classes take the concept of virtual classes a step further by enforcing implementation requirements on subclasses. They can’t be instantiated directly and often include abstract methods that must be implemented by any concrete subclass.
Key Characteristics of Abstract Classes
- Declared using the
abstractkeyword - Cannot be instantiated directly
- Can contain both implemented and abstract methods
- Abstract methods must be implemented by subclasses
Abstract Class Example
public abstract class NotificationService {
// Concrete method available to all subclasses
public void logNotification(String message) {
System.debug('Notification sent: ' + message);
// Log to custom object, etc.
}
// Abstract method that subclasses MUST implement
public abstract void sendNotification(String recipient, String message);
// Template method pattern - defines the algorithm structure
public void processNotification(String recipient, String message) {
// Pre-processing
message = formatMessage(message);
// Send notification (implementation varies by subclass)
sendNotification(recipient, message);
// Post-processing
logNotification(message);
}
// Virtual method that can be optionally overridden
protected virtual String formatMessage(String message) {
return '[NOTIFICATION] ' + message;
}
}
Implementing Abstract Classes
public class EmailNotificationService extends NotificationService {
// Must implement the abstract method
public override void sendNotification(String recipient, String message) {
// Email-specific implementation
Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
email.setToAddresses(new String[] { recipient });
email.setSubject('Notification');
email.setPlainTextBody(message);
try {
Messaging.sendEmail(new Messaging.SingleEmailMessage[] { email });
} catch(Exception e) {
System.debug('Email sending failed: ' + e.getMessage());
}
}
// Optional override of the virtual method
protected override String formatMessage(String message) {
return '[EMAIL NOTIFICATION] ' + message + '\n\nThank you,\nThe Salesforce Team';
}
}
public class SMSNotificationService extends NotificationService {
// Must implement the abstract method
public override void sendNotification(String recipient, String message) {
// SMS-specific implementation
System.debug('Sending SMS to ' + recipient + ': ' + message);
// Integration with SMS service
}
}
Interfaces in Salesforce
Interfaces provide the most flexible form of abstraction. They define a contract that implementing classes must fulfill but provide no implementation details. This allows for creating truly pluggable components in your Salesforce applications.
Key Characteristics of Interfaces
- Declared using the
interfacekeyword - Cannot contain implemented methods, only method signatures
- Classes can implement multiple interfaces
- All methods in an interface are implicitly public and abstract
Interface Example
public interface Schedulable {
void execute(SchedulableContext sc);
}
public interface DataProcessor {
List<SObject> processRecords(List<SObject> records);
void handleErrors(List<Error__c> errors);
}
Implementing Multiple Interfaces
public class AccountBatchProcessor implements Database.Batchable<SObject>, Database.Stateful, DataProcessor {
private Integer recordsProcessed = 0;
private List<Error__c> errorLogs = new List<Error__c>();
// From Batchable interface
public Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator('SELECT Id, Name FROM Account WHERE CreatedDate = TODAY');
}
// From Batchable interface
public void execute(Database.BatchableContext bc, List<Account> scope) {
List<Account> processedRecords = (List<Account>)processRecords(scope);
try {
update processedRecords;
recordsProcessed += processedRecords.size();
} catch(Exception e) {
Error__c errorLog = new Error__c(
Process_Name__c = 'AccountBatchProcessor',
Error_Message__c = e.getMessage(),
Stack_Trace__c = e.getStackTraceString()
);
errorLogs.add(errorLog);
}
}
// From Batchable interface
public void finish(Database.BatchableContext bc) {
System.debug('Total records processed: ' + recordsProcessed);
handleErrors(errorLogs);
}
// From DataProcessor interface
public List<SObject> processRecords(List<SObject> records) {
List<Account> accounts = (List<Account>)records;
for(Account acc : accounts) {
acc.Description = 'Updated by batch on ' + System.today();
}
return accounts;
}
// From DataProcessor interface
public void handleErrors(List<Error__c> errors) {
if(!errors.isEmpty()) {
insert errors;
}
}
}
Real-World Implementation: A Design Pattern Example
Let’s implement a Strategy Pattern using Salesforce Apex to demonstrate how these concepts work together in a practical scenario. We’ll create a lead scoring system that can use different algorithms based on business requirements.

Define the Interface
public interface LeadScoringStrategy {
Integer calculateScore(Lead lead);
}
Implement Different Strategies
public class IndustryBasedScoring implements LeadScoringStrategy {
private Map<String, Integer> industryScores = new Map<String, Integer>{
'Technology' => 30,
'Healthcare' => 25,
'Financial Services' => 35,
'Retail' => 20,
'Manufacturing' => 15
};
public Integer calculateScore(Lead lead) {
Integer score = industryScores.containsKey(lead.Industry) ?
industryScores.get(lead.Industry) : 10;
// Add points for complete information
if(String.isNotBlank(lead.Email)) score += 10;
if(String.isNotBlank(lead.Phone)) score += 5;
if(lead.NumberOfEmployees != null) score += 5;
return score;
}
}
public class EngagementBasedScoring implements LeadScoringStrategy {
public Integer calculateScore(Lead lead) {
Integer score = 0;
// Query related task activities
List<Task> activities = [SELECT Id, Subject, ActivityDate
FROM Task
WHERE WhoId = :lead.Id
ORDER BY ActivityDate DESC];
// Score based on engagement
score += activities.size() * 5; // 5 points per activity
// Score email opens or clicks (assuming custom fields)
score += Integer.valueOf(lead.Email_Opens__c != null ? lead.Email_Opens__c : 0) * 2;
score += Integer.valueOf(lead.Email_Clicks__c != null ? lead.Email_Clicks__c : 0) * 3;
return score;
}
}
Abstract Service Class
public abstract class LeadScoringService {
protected LeadScoringStrategy scoringStrategy;
// Constructor injection of the strategy
public LeadScoringService(LeadScoringStrategy strategy) {
this.scoringStrategy = strategy;
}
// Method to change strategy at runtime
public void setScoringStrategy(LeadScoringStrategy strategy) {
this.scoringStrategy = strategy;
}
// Template method for lead scoring process
public List<Lead> scoreLeads(List<Lead> leads) {
List<Lead> scoredLeads = new List<Lead>();
for(Lead lead : leads) {
Integer score = scoringStrategy.calculateScore(lead);
lead.Lead_Score__c = score;
// Common logic regardless of scoring strategy
lead.Lead_Score_Date__c = System.today();
lead.Lead_Score_Status__c = getLeadStatus(score);
scoredLeads.add(lead);
}
return scoredLeads;
}
// Concrete method used by template method
protected String getLeadStatus(Integer score) {
if(score >= 75) return 'Hot';
else if(score >= 50) return 'Warm';
else if(score >= 25) return 'Cool';
else return 'Cold';
}
// Abstract method for strategy-specific validation
public abstract Boolean validateScoring(Lead lead);
}
Concrete Service Implementation
public class EnterpriseLeadScoringService extends LeadScoringService {
public EnterpriseLeadScoringService(LeadScoringStrategy strategy) {
super(strategy);
}
public override Boolean validateScoring(Lead lead) {
// Enterprise-specific validation logic
if(lead.Company == null || lead.Industry == null) {
return false;
}
// Ensure we have contact information
if(lead.Email == null && lead.Phone == null) {
return false;
}
return true;
}
// Override the template method to add enterprise-specific behavior
public List<Lead> scoreEnterpriseLeads(List<Lead> leads) {
List<Lead> validLeads = new List<Lead>();
for(Lead lead : leads) {
if(validateScoring(lead)) {
validLeads.add(lead);
}
}
// Call the parent implementation to score valid leads
return super.scoreLeads(validLeads);
}
}
Usage Example
public class LeadScoringBatch implements Database.Batchable<SObject> {
private LeadScoringService scoringService;
public LeadScoringBatch() {
// Select scoring strategy based on custom setting or metadata
LeadScoringStrategy strategy;
String strategyType = LeadScoringSettings__c.getInstance().Strategy_Type__c;
if(strategyType == 'Industry') {
strategy = new IndustryBasedScoring();
} else {
strategy = new EngagementBasedScoring();
}
scoringService = new EnterpriseLeadScoringService(strategy);
}
public Database.QueryLocator start(Database.BatchableContext BC) {
return Database.getQueryLocator('SELECT Id, Name, Company, Industry, Email, Phone, ' +
'NumberOfEmployees, Email_Opens__c, Email_Clicks__c ' +
'FROM Lead WHERE IsConverted = false');
}
public void execute(Database.BatchableContext BC, List<Lead> scope) {
List<Lead> scoredLeads = scoringService.scoreLeads(scope);
update scoredLeads;
}
public void finish(Database.BatchableContext BC) {
// Send completion notification
}
}
Benefits and Best Practices
Benefits of Using Virtual/Abstract Classes and Interfaces
- Code Reuse: Common functionality can be defined once and reused across multiple classes.
- Flexibility: Systems can adapt to new requirements by swapping implementations.
- Testability: Abstract components make it easier to mock dependencies during testing.
- Maintainability: Changes can be made to implementations without affecting client code.
- Organization: Clear separation of concerns makes code easier to understand.
Best Practices
- Follow the Interface Segregation Principle: Create small, focused interfaces rather than large, monolithic ones.
- Favor Composition Over Inheritance: When possible, use interfaces to compose behavior rather than creating deep inheritance hierarchies.
- Use Abstract Classes for Template Methods: When you want to define an algorithm structure but allow steps to be customized.
- Document Your Abstractions: Clearly document the contract and expected behavior of abstract methods.
- Balance Abstraction with Pragmatism: Don’t over-engineer; use these patterns when they provide clear benefits.
Conclusion
In Salesforce development, virtual/abstract classes and interfaces provide powerful tools for creating flexible, maintainable applications. They allow developers to implement design patterns that promote code reuse, simplify testing, and enable systems to evolve.
What abstraction patterns have you found most useful in your Salesforce development projects? Share your experiences in the comments below!
