LearnFrenzy
Sign in Take a free test

Object-Oriented Apex · Lesson 12 of 41

Inheritance, Interfaces and Polymorphism in Apex

5 min read Updated Nov 8, 2026 Saurabh Samir

Once a codebase grows past a few classes, you start seeing the same shapes repeat: several kinds of discount, several notification channels, several integrations that all "send" something. Inheritance and interfaces let you write the shared part once and swap the specific part – and they're the foundation of trigger frameworks and enterprise patterns you'll meet later.

In this lesson

  • virtual, abstract and override
  • super and protected
  • Interfaces and polymorphism
  • Choosing an implementation at runtime with Type.forName
  • Comparable for sorting your own classes

Classes are closed by default

In Java any class can be extended. In Apex a class must be declared virtual or abstract to allow it, and a method must be virtual or abstract to be overridden – with the override keyword on the child. It's deliberate: you decide what can change.

// Notification.cls
public abstract class Notification {
    protected String recipient;                       // visible to subclasses

    public Notification(String recipient) {
        this.recipient = recipient;
    }

    public abstract String channel();                 // subclasses MUST implement

    public virtual String format(String message) {    // subclasses MAY override
        return '[' + channel() + '] to ' + recipient + ': ' + message;
    }

    public String send(String message) {              // not virtual: same for everyone
        return 'Sent ' + format(message);
    }
}
// EmailNotification.cls
public class EmailNotification extends Notification {
    public EmailNotification(String recipient) {
        super(recipient);                             // call the parent constructor
    }
    public override String channel() {
        return 'EMAIL';
    }
}
// SmsNotification.cls
public class SmsNotification extends Notification {
    public SmsNotification(String recipient) {
        super(recipient);
    }
    public override String channel() {
        return 'SMS';
    }
    public override String format(String message) {
        String text = super.format(message);          // reuse the parent version
        return text.length() > 50 ? text.left(47) + '...' : text;
    }
}
List<Notification> outbox = new List<Notification>{
    new EmailNotification('asha@acme.com'),
    new SmsNotification('+91-98765-43210')
};
for (Notification n : outbox) {
    System.debug(n.send('Your case 00001042 has been resolved. Thank you!'));
}

Debug log

Sent [EMAIL] to asha@acme.com: Your case 00001042 has been resolved. Thank you!
Sent [SMS] to +91-98765-43210: Your case 00001042 ha...

The loop doesn't know or care which kind of notification it holds – each object runs its own version of channel() and format(). That's polymorphism.

KeywordMeaning
virtual (class)can be extended and instantiated
abstract (class)can be extended, can't be instantiated; may have abstract methods
virtual (method)has a body; children may override it
abstract (method)no body; children must implement it
overriderequired on the child's version
superthe parent's constructor or method

Interfaces

An interface is a contract: a list of method signatures with no implementation. A class can extend only one class but implement many interfaces, which makes interfaces the more flexible tool for "things that can do X":

// DiscountRule.cls
public interface DiscountRule {
    Decimal discountFor(Opportunity opp);
    String name();
}
// VolumeDiscount.cls
public class VolumeDiscount implements DiscountRule {
    public Decimal discountFor(Opportunity opp) {
        return opp.Amount >= 100000 ? opp.Amount * 0.05 : 0;
    }
    public String name() { return 'Volume'; }
}
// LoyaltyDiscount.cls
public class LoyaltyDiscount implements DiscountRule {
    public Decimal discountFor(Opportunity opp) {
        return opp.Type == 'Existing Customer - Upgrade' ? 2500 : 0;
    }
    public String name() { return 'Loyalty'; }
}
Opportunity opp = new Opportunity(Name = 'Acme upgrade', Amount = 150000, Type = 'Existing Customer - Upgrade');

List<DiscountRule> rules = new List<DiscountRule>{ new VolumeDiscount(), new LoyaltyDiscount() };
Decimal totalDiscount = 0;
for (DiscountRule rule : rules) {
    Decimal d = rule.discountFor(opp);
    System.debug(rule.name() + ': ' + d);
    totalDiscount += d;
}
System.debug('Total discount: ' + totalDiscount);

Debug log

Volume: 7500.00
Loyalty: 2500
Total discount: 10000.00

Adding a new discount means adding a new class – the calculating loop never changes. This "open for extension, closed for modification" idea is what makes large codebases maintainable.

Choosing the implementation at runtime

Type.forName creates an object from a class name stored as text – in custom metadata, a custom setting or a field. Admins can switch behaviour without a deployment:

// imagine these names come from Custom Metadata records
List<String> configuredRules = new List<String>{ 'LoyaltyDiscount', 'VolumeDiscount', 'NoSuchRule' };

for (String className : configuredRules) {
    Type t = Type.forName(className);
    if (t == null) {
        System.debug('Skipping unknown rule ' + className);
        continue;
    }
    DiscountRule rule = (DiscountRule) t.newInstance();
    System.debug('Loaded ' + rule.name() + ' rule');
}

Debug log

Loaded Loyalty rule
Loaded Volume rule
Skipping unknown rule NoSuchRule

Comparable: sorting your own classes

// LeadScore.cls
public class LeadScore implements Comparable {
    public String name;
    public Integer score;

    public LeadScore(String name, Integer score) {
        this.name = name;
        this.score = score;
    }

    public Integer compareTo(Object other) {            // highest score first
        LeadScore that = (LeadScore) other;
        return that.score - this.score;
    }
}
List<LeadScore> leads = new List<LeadScore>{
    new LeadScore('Globex', 62), new LeadScore('Acme', 91), new LeadScore('Initech', 75)
};
leads.sort();
for (LeadScore l : leads) System.debug(l.name + ' ' + l.score);

Debug log

Acme 91
Initech 75
Globex 62

Implement Comparable for a class's one natural order; use a separate Comparator (previous chapter) when you need several orders.

Practice

Try it yourself

  1. Add a SeasonalDiscount rule that gives 3% in December. What changes in the calculating loop?

    Show answer

    Nothing. Write public class SeasonalDiscount implements DiscountRule { … } and add an instance to the rules list (or its name to the configuration).

  2. Why does new Notification('x') fail to compile?

    Show answer

    Notification is abstract – it can't be instantiated, only extended.

Quick check

Can you extend an Apex class that isn't virtual or abstract?

No.

How many interfaces can a class implement?

Many – but it can extend only one class.

What does Type.forName return for an unknown class name?

null.

Summary

  • Mark classes and methods virtual or abstract to allow extension; use override and super.
  • Interfaces define contracts; code against the interface, not the class.
  • Polymorphism lets one loop handle many implementations.
  • Type.forName enables configuration-driven behaviour; Comparable gives a natural sort order.

Finished this lesson?

Mark it complete to track your progress, then carry on with the next lesson.

Next lesson

Search LearnFrenzy

Esc

Quick links