LearnFrenzy
Sign in Take a free test

Object-Oriented Apex · Lesson 11 of 41

Apex Classes, Objects, Constructors and Properties

6 min read Updated Nov 8, 2026 Saurabh Samir

A class bundles data and the behaviour that goes with it. In Salesforce projects you'll write service classes that hold business logic, controller classes for LWC, wrapper classes that shape data for the UI, and small value classes like a Money or an Address. This lesson covers everything you need to write them well – including how static behaves differently in Apex than in Java.

In this lesson

  • Class anatomy and access modifiers
  • Constructors and constructor chaining
  • Properties with get and set
  • Static vs instance – and static lifetime in Apex
  • Inner (wrapper) classes and enums

A class, piece by piece

// Invoice.cls
public with sharing class Invoice {
    // fields (state) – private: only this class can touch them
    private String customer;
    private List<Decimal> lineAmounts = new List<Decimal>();

    // properties: controlled access from outside
    public String invoiceNumber { get; private set; }            // anyone reads, only the class writes
    public Decimal taxRate { get; set; }                    // a plain read/write property

    public Decimal total {                                  // computed property: no stored value
        get {
            Decimal sum = 0;
            for (Decimal a : lineAmounts) sum += a;
            return (sum * (1 + (taxRate ?? 0))).setScale(2);
        }
    }

    // constructors
    public Invoice(String customer, String invoiceNumber) {
        this.customer = customer;                           // this. = the field, not the parameter
        this.invoiceNumber = invoiceNumber;
        this.taxRate = 0.18;
    }

    public Invoice(String customer) {
        this(customer, 'DRAFT');                            // call the other constructor
    }

    // methods
    public Invoice addLine(Decimal amount) {
        if (amount == null || amount <= 0) {
            throw new IllegalArgumentException('Line amount must be positive');
        }
        lineAmounts.add(amount);
        return this;                                        // returning this allows chaining
    }

    public override String toString() {
        return invoiceNumber + ' for ' + customer + ': ' + lineAmounts.size() + ' lines, total ' + total;
    }
}
Invoice inv = new Invoice('Acme', 'INV-1001');
inv.addLine(1000).addLine(250.50);
System.debug(inv);
System.debug('Total: ' + inv.total);

inv.taxRate = 0.05;
System.debug('With 5% tax: ' + inv.total);

Invoice draft = new Invoice('Globex');
System.debug(draft.invoiceNumber);
// draft.invoiceNumber = 'X';   ← compile error: the setter is private

Debug log

INV-1001 for Acme: 2 lines, total 1475.59
Total: 1475.59
With 5% tax: 1313.02
DRAFT

Reserved words

The first draft of this class used a property called number – and the org refused it: "Identifier name is reserved: number". Apex reserves many words beyond the obvious keywords, including number, array, object, group, limit and select. If a sensible name won't compile, a reserved word is the likely reason.

Access modifiers

ModifierVisible to
privatethis class only – the default for members if you write nothing
protectedthis class and classes that extend it
publicany Apex in your org's namespace
globalany Apex anywhere – required for some APIs (e.g. @RestResource, managed package entry points)

Start private and widen only when something genuinely needs access. A class with mostly private fields and a few public methods is far easier to change safely later.

Static vs instance – and how long static lives

Instance members belong to each object; static members belong to the class. The Apex twist: static variables live for one transaction, not for the life of a server. Every request starts fresh. Within a transaction, though, they're shared – which is exactly why triggers use them to avoid running twice.

// RequestCounter.cls
public class RequestCounter {
    public static Integer calls = 0;                        // shared across the transaction
    public Integer myCalls = 0;                             // one per object

    public void hit() {
        calls++;
        myCalls++;
    }
}
RequestCounter a = new RequestCounter();
RequestCounter b = new RequestCounter();
a.hit();
a.hit();
b.hit();
System.debug('a.myCalls = ' + a.myCalls + ', b.myCalls = ' + b.myCalls);
System.debug('RequestCounter.calls = ' + RequestCounter.calls);

Debug log

a.myCalls = 2, b.myCalls = 1
RequestCounter.calls = 3

Run this script again and calls starts at 0 again – no state survives between transactions. To keep data across requests you use the database, Platform Cache or custom settings.

Wrapper (inner) classes

A wrapper class combines data that doesn't live on one object – a record plus a checkbox state, or values from several objects for one screen. You'll write them constantly for LWC:

// AccountSummaryService.cls
public with sharing class AccountSummaryService {

    public class AccountSummary {                           // inner class
        @AuraEnabled public String name;
        @AuraEnabled public Integer contactCount;
        @AuraEnabled public Boolean isKeyAccount;

        public AccountSummary(Account acc, Integer contactCount) {
            this.name = acc.Name;
            this.contactCount = contactCount;
            this.isKeyAccount = contactCount >= 2;
        }
    }

    public static List<AccountSummary> summarise() {
        List<AccountSummary> result = new List<AccountSummary>();
        for (Account acc : [SELECT Name, (SELECT Id FROM Contacts) FROM Account ORDER BY Name]) {
            result.add(new AccountSummary(acc, acc.Contacts.size()));
        }
        return result;
    }
}
List<Account> accs = new List<Account>{ new Account(Name = 'Acme'), new Account(Name = 'Globex') };
insert accs;
insert new List<Contact>{
    new Contact(LastName = 'Rao', AccountId = accs[0].Id),
    new Contact(LastName = 'Iyer', AccountId = accs[0].Id),
    new Contact(LastName = 'Shah', AccountId = accs[1].Id)
};

for (AccountSummaryService.AccountSummary s : AccountSummaryService.summarise()) {
    System.debug(s.name + ': ' + s.contactCount + ' contact(s), key account: ' + s.isKeyAccount);
}
System.debug(JSON.serialize(AccountSummaryService.summarise()[0]));

Debug log

Acme: 2 contact(s), key account: true
Globex: 1 contact(s), key account: false
{"name":"Acme","isKeyAccount":true,"contactCount":2}

Inner classes are referenced as Outer.Inner from outside. Apex allows only one level of nesting. The @AuraEnabled annotations make the properties visible when the wrapper is returned to a Lightning component.

Enums

// Season.cls
public enum Season { WINTER, SPRING, SUMMER, AUTUMN }
Season s = Season.SUMMER;
System.debug(s + ' name=' + s.name() + ' ordinal=' + s.ordinal());
System.debug(Season.values());
System.debug(Season.valueOf('WINTER') == Season.WINTER);

Debug log

SUMMER name=SUMMER ordinal=2
(WINTER, SPRING, SUMMER, AUTUMN)
true

Enums give you a fixed set of named values that the compiler checks – much safer than passing around strings like 'summer'. They work beautifully with switch.

Practice

Try it yourself

  1. Add a read-only property lineCount to Invoice that returns the number of lines.

    Show answer

    public Integer lineCount { get { return lineAmounts.size(); } } – a property with only a getter can't be set from outside.

  2. A trigger handler uses static Boolean alreadyRan to stop recursion. A user saves a record, and then saves it again a minute later. Does the second save run the logic?

    Show answer

    Yes – static variables reset with every transaction. The flag only prevents re-entry within the same save.

Quick check

What is the default access of a class member with no modifier?

private.

How long does a static variable keep its value in Apex?

For the current transaction only.

How do you call one constructor from another?

this(arguments) as the first statement.

Summary

  • Keep fields private, expose behaviour through methods and properties.
  • Overload constructors and chain them with this(...).
  • Static members are shared per transaction; instance members per object.
  • Use inner wrapper classes to shape data and enums for fixed choices.

Finished this lesson?

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

Next lesson

Search LearnFrenzy

Esc

Quick links