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 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.
virtual keywordvirtual that subclasses can overridepublic 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;
}
}
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 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.
abstract keywordpublic 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;
}
}
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 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.
interface keywordpublic interface Schedulable {
void execute(SchedulableContext sc);
}
public interface DataProcessor {
List<SObject> processRecords(List<SObject> records);
void handleErrors(List<Error__c> errors);
}
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;
}
}
}
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.

public interface LeadScoringStrategy {
Integer calculateScore(Lead lead);
}
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;
}
}
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);
}
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);
}
}
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
}
}
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!