Object-Oriented Apex · Lesson 11 of 41
Apex Classes, Objects, Constructors and Properties
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
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
| Modifier | Visible to |
|---|---|
private | this class only – the default for members if you write nothing |
protected | this class and classes that extend it |
public | any Apex in your org's namespace |
global | any 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);
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]));
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);
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
Add a read-only property
lineCountto 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.A trigger handler uses
static Boolean alreadyRanto 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.