Apex Classes and Objects: A Beginner's Tour
Learn Apex OOP from the ground up: classes, objects, constructors, instance vs static methods, and when each fits — with examples and challenges.
TL;DR
Apex is a class-based object-oriented language, which means almost every line of
business logic you write lives inside a class. A class is the blueprint; an
object is one concrete instance of that blueprint. Once you can read a class
header, spot a constructor, and decide whether a method belongs on the instance
or on the static side, the rest of Apex starts to feel like one consistent
shape instead of a pile of List<Account> snippets. The post below walks
through each piece with runnable code, then points you at the exercises where
you can practice it on Lightning Challenge.
Why Apex Is Object-Oriented (And Why It Matters)
Every piece of Apex code that lives in your org — triggers, batch classes,
REST endpoints, test classes — is a class. sObject declarations and method
calls only exist inside a method, which is itself a member of a class.
Understanding that single fact clears up a lot of beginner confusion: when
you read
Account a = new Account(Name = 'Acme');what is happening is that Account is a class (defined in the Salesforce
platform), a is an object of that class, and new Account(...) calls a
constructor. There is no other kind of code in Apex.
This matters because most of the bugs that bite new Salesforce developers come
from mixing up instance state (per-object data) with class-level state
(static data). A method on an Account works on one specific account; a
static method on a helper class cannot see instance fields because there is
no instance. Keeping that distinction clean is half of writing Apex that does
what you expect.
Classes and Objects: The Blueprint Metaphor
The shortest useful definition:
- A class is the source file. It declares what fields the objects will have and what methods they can run.
- An object (or instance) is what you get when you call
new MyClass(...). Each object has its own copy of the instance fields.
The cleanest way to internalize this is to write one yourself. Save this into
the Developer Console as SupportTicket.cls:
public class SupportTicket {
public String subject;
public String priority; // 'Low', 'Normal', 'High'
public SupportTicket(String subject, String priority) {
this.subject = subject;
this.priority = priority;
}
public String summary() {
return '[' + priority + '] ' + subject;
}
}SupportTicket is the class. In your next line of code, new SupportTicket( 'Login broken', 'High') returns an object — a brand-new ticket with its own
subject and priority. Calling ticket.summary() reads that object's fields
and returns the string '[High] Login broken'. If you build a second ticket,
it has a separate copy of the same fields. That separation — many objects,
one shared definition — is the entire point of OOP.
Constructors Run Once, At new
A constructor is a method that has no return type and shares its name with the class. It runs exactly once, at the moment the object is created, and its job is to put the object into a usable state. If you skip it, Apex gives you a no-argument constructor only if you did not write any other one.
public class TeamMember {
public String name;
public Integer ticketsResolved;
public TeamMember(String name) {
this.name = name;
this.ticketsResolved = 0;
}
}The line this.ticketsResolved = 0; is doing real work: every new
TeamMember starts with a clean count, so two team members do not share a
single counter. Without a constructor, every TeamMember would have
ticketsResolved default to null, and null + 1 blows up with a
NullPointerException the moment you forget to set the field.
The this keyword is the object speaking about itself. this.name = name; is
shorthand for "take the value the caller passed in as name, and store it in
my instance field also called name." It is the cleanest way to
disambiguate the two, and the convention carries to every other field you
assign.
Instance vs Static Methods
Here is the decision that trips up most learners. Instance methods can read and write the object's fields. Static methods cannot — they belong to the class, not to any specific object.
public class Invoice {
private static final Decimal TAX_RATE = 0.07; // one value for the whole class
public static Decimal withTax(Decimal subtotal) {
return subtotal + (subtotal * TAX_RATE);
}
}You call Invoice.withTax(100) directly on the class — no new Invoice()
needed. That is correct because the math does not depend on any particular
invoice's data. If a method needs this.subtotal, it cannot be static.
A useful rule of thumb while you are still building intuition: start with
instance methods, switch a method to static only when nothing it reads
belongs to a specific object.
A static final Decimal field is the closest thing Apex has to a true
constant: the value is fixed at compile time and shared by every instance of
Invoice. That is a perfect place for a tax rate or any other number that
should not vary per object.
Fields Belong to the Object, Not to the Class
static fields are shared across every instance of a class. Instance fields
are not. That difference is what makes a class-level cache possible, and
what makes a per-record counter trivial.
public class StepCounter {
public Integer steps;
public StepCounter(Integer steps) {
this.steps = steps;
}
}When you build two StepCounter objects — a morning counter and an evening
counter — each one holds its own steps value independently. The same source
file could not give them the same number without setting it each time you
called new. As soon as you accidentally declared a field static, every
StepCounter in the entire org would share one number — and that is almost
never what you wanted.
A Tiny End-to-End Example
Putting the pieces together, here is a class plus the code that builds one of its objects:
public class CoffeeOrder {
public String size; // 'small', 'medium', 'large'
public Integer shots;
public CoffeeOrder(String size, Integer shots) {
this.size = size;
this.shots = shots;
}
public Integer totalShots() {
return this.shots;
}
}Calling new CoffeeOrder('large', 2) returns an object whose size is
'large' and whose shots is 2. Calling mine.totalShots() on that object
returns 2. Two objects of the same class give you two independent values,
one definition, one constructor, one instance method, no statics where they
do not belong. That is the shape you will see in every Apex tutorial and
every Salesforce codebase you read.
Common Beginner Mistakes
A few patterns show up over and over in beginner Apex:
- Forgetting the
new.Account a = Account();does not compile, andAccount a;(no initializer) leavesaas a reference to nothing. - Treating
staticlike "global".staticis class-level, which is closer to "shared by every instance" than to "global variable". It is scoped to one class, not the whole transaction. - Putting business logic in a trigger handler's constructor. A
constructor runs when the object is built — generally the test framework.
Business work belongs in instance methods (or
staticmethods) you call yourself, not in the constructor. - Assuming
==on objects compares contents. In Apex,==is reference equality for objects. TwoAccountrecords with the same fields are not==to each other. Compare individual fields instead.
The fastest way to internalize these is to write a class, change it, and see what compiles and what does not. That is exactly what the exercises below are designed for.
Where To Practice Next
The class-and-object mechanics above are the entry point to the Object-Oriented Apex lesson page. Once you have read through the lesson, the fastest way to lock the patterns in is to build a small class from scratch on Lightning Challenge. These beginner challenges all start from a blank class and ask you to fill in the same shapes you just read about:
- Build a Support Ticket Constructor — the same constructor pattern as in this post, applied to a real record.
- Step Counter Totals — instance fields and a method that reads them, the cleanest possible OOP starter.
- Team Lead Budget — multiple objects of the same class with shared behavior; where constructors start to feel necessary rather than optional.
- Coffee Order Total — a class with a couple of fields and one method that combines them, mirroring the final example.
If you want the harder OOP patterns, Polymorphism with Interfaces takes the same class system you used here and adds an interface layer on top. Doing that one right after a beginner OOP block is the most natural next step.
About Warren Walters
Salesforce MVP and transformative mentor with 8+ years in the Salesforce realm. Founder of Lightning Challenge, dedicated to nurturing the next generation of Salesforce talent through hands-on practice and real-world coding challenges.
Visit Profile →