Part 37: Apex Basic Concepts in Salesforce
Welcome back to the Salesforce series. This post marks a major transition. For the first 36 parts, we focused on administration — configuration, declarative automation, security, reporting, and platform features that do not require writing code. Starting with this post, we are entering Topic 3: The Complete Guide To Apex, and the focus shifts entirely to development.
If you have been following the series as an admin, do not worry. Everything you have learned so far — objects, fields, relationships, record access, flows, validation rules — forms the foundation that Apex builds on top of. Developers do not replace admins; they extend what admins build. The best Salesforce developers are the ones who understand the platform deeply enough to know when code is necessary and when a declarative solution is the better choice.
This post covers the fundamentals of the Apex programming language from the ground up. By the end of it, you will understand what Apex is, how to set up your development environment, and how to write classes with variables, methods, constructors, collections, and access modifiers. Let us get started.
What to Expect as a Salesforce Developer
Before diving into syntax, it helps to understand what Salesforce developers actually do on a day-to-day basis.
Day-to-Day Responsibilities
- Writing Apex triggers and classes to implement business logic that cannot be handled declaratively.
- Building Lightning Web Components (LWC) and Aura components for custom user interfaces.
- Writing SOQL and SOSL queries to retrieve data from the Salesforce database.
- Integrating Salesforce with external systems using REST and SOAP APIs, callouts, and platform events.
- Writing test classes to ensure code coverage and validate business logic. Salesforce requires a minimum of 75% code coverage before deploying to production.
- Debugging and troubleshooting issues using debug logs, the Developer Console, and IDE tools.
- Performing code reviews and collaborating with other developers through version control systems like Git.
- Deploying code through change sets, the Salesforce CLI, or CI/CD pipelines.
How Development Differs from Administration
Admins configure the platform using clicks — they create objects, fields, page layouts, validation rules, and flows. Developers write code to handle scenarios that go beyond what clicks can accomplish. Here are some examples of when code is needed:
- Complex business logic that involves multiple objects, conditional branching, and calculations that would be unwieldy in a flow.
- Custom user interfaces that the standard Lightning App Builder cannot produce.
- Integrations with external systems that require HTTP callouts, authentication handling, and response parsing.
- Batch processing of large data volumes that exceed what flows can handle efficiently.
- Custom API endpoints that expose Salesforce data to external applications.
The line between admin and developer work is not always sharp. Many tasks sit in a gray area where either approach could work. As a developer, your job is to pick the right tool for the job.
The Developer Ecosystem
Salesforce developers have access to a rich ecosystem:
- Trailhead — Free learning platform with hands-on modules and projects.
- Salesforce Developer Documentation — Comprehensive reference for Apex, LWC, APIs, and metadata.
- Salesforce Stack Exchange — Community Q&A site for technical questions.
- Salesforce CLI (sf/sfdx) — Command-line tool for org management, deployment, and data operations.
- Salesforce Extensions for VS Code — Official IDE extension pack.
- GitHub and Open Source — A growing number of open-source libraries, frameworks, and utilities built for the Salesforce platform.
How to Setup a Free Salesforce Developer Org
Every Salesforce developer needs a Developer Edition org. It is free, never expires, and gives you a fully functional Salesforce environment to practice in.
Step-by-Step Signup
- Open your browser and navigate to developer.salesforce.com.
- Click the Sign Up button.
- Fill out the registration form with your first name, last name, email, role, company, country, and postal code.
- Choose a username. This must be in email format (e.g.,
yourname@dev-practice.com) but does not need to be a real email address. It must be globally unique across all Salesforce orgs. - Click Sign me up.
- Check your email inbox for a verification email from Salesforce.
- Click the verification link and set your password.
- Log in to your new Developer Edition org.
You now have an org with access to all standard Salesforce features, the Developer Console, and the ability to write and deploy Apex code. You can create multiple Developer Edition orgs if you want separate environments for different projects.
What is the Apex Programming Language?
Apex is Salesforce’s proprietary programming language. It is the language you use to write custom business logic that runs on the Salesforce platform.
Key Characteristics
- Java-like syntax: If you have experience with Java, C#, or similar languages, Apex will feel familiar. It uses classes, interfaces, methods, loops, conditionals, and exception handling in a style very close to Java.
- Strongly typed: Every variable must have a declared type. You cannot assign a String to an Integer variable without explicit conversion.
- Object-oriented: Apex supports classes, interfaces, inheritance, polymorphism, and encapsulation.
- Runs on Salesforce servers: Apex code does not run on your local machine or in the browser. It executes on Salesforce’s multi-tenant servers. This is important because it means Salesforce controls the execution environment.
- Governor limits: Because Salesforce is a multi-tenant platform (many customers share the same infrastructure), Apex code is subject to governor limits. These limits restrict how many SOQL queries, DML statements, CPU milliseconds, and heap memory your code can use in a single transaction. Governor limits exist to prevent any single tenant’s code from monopolizing shared resources.
- Compiled and executed on the platform: When you save Apex code, Salesforce compiles it into a set of instructions that the platform’s runtime engine executes. You do not compile locally and upload a binary.
- Integrated with the database: Apex has native support for SOQL (Salesforce Object Query Language) and DML (Data Manipulation Language) operations. You can query and manipulate records directly in your code without needing an ORM or database driver.
- Test framework built in: Apex includes a built-in testing framework. You write test methods, use
@isTestannotations, and Salesforce tracks code coverage as a first-class metric.
A Simple Example
Here is the simplest possible Apex class:
public class HelloWorld {
public static void sayHello() {
System.debug('Hello, World!');
}
}
System.debug() prints output to the debug log. It is the Apex equivalent of console.log() in JavaScript or System.out.println() in Java.
What is an IDE?
An IDE (Integrated Development Environment) is a software application that provides tools for writing, testing, and debugging code in a single interface. While you can write Apex directly in the Salesforce Developer Console (a browser-based tool), a proper IDE gives you significant advantages:
- Syntax highlighting and autocomplete — The IDE understands Apex syntax and offers suggestions as you type.
- Error detection — Many errors are caught before you even save the file.
- Project-level file management — You can see all your classes, triggers, components, and metadata files in a project tree.
- Source control integration — Connect to Git repositories directly from the IDE.
- Deployment tools — Push code to your org and retrieve changes with a few clicks or commands.
- Debugging — Set breakpoints, inspect variables, and step through replay debug logs.
The two most popular IDE choices for Salesforce development are Visual Studio Code and IntelliJ IDEA with Illuminated Cloud 2.
Installing and Setting Up Visual Studio Code
Visual Studio Code (VS Code) is the officially recommended IDE for Salesforce development. It is free, lightweight, and has a rich extension ecosystem.
Step 1: Install VS Code
Download and install VS Code from code.visualstudio.com. It is available for Windows, macOS, and Linux.
Step 2: Install the Salesforce CLI
The Salesforce CLI is required for the VS Code extensions to communicate with your org. Install it by visiting developer.salesforce.com/tools/salesforcecli and following the instructions for your operating system. After installation, verify it by opening a terminal and running:
sf --version
Step 3: Install the Salesforce Extension Pack
- Open VS Code.
- Go to the Extensions view (Ctrl+Shift+X on Windows/Linux, Cmd+Shift+X on macOS).
- Search for Salesforce Extension Pack.
- Click Install.
This installs a bundle of extensions including Apex language support, SOQL editing, Lightning Web Components support, and deployment tools.
Step 4: Create a Salesforce Project
- Open the Command Palette (Ctrl+Shift+P or Cmd+Shift+P).
- Type SFDX: Create Project and select it.
- Choose Standard as the template.
- Name your project and choose a location on your file system.
VS Code creates a project folder with the standard Salesforce DX directory structure.
Step 5: Authorize Your Org
- Open the Command Palette.
- Type SFDX: Authorize an Org and select it.
- Choose Production (for a Developer Edition org, this is the correct option).
- A browser window opens. Log in with your Developer Edition credentials.
- Once authenticated, VS Code is connected to your org.
Step 6: Retrieve and Deploy
- Retrieve: Right-click a metadata file or folder and choose SFDX: Retrieve Source from Org to pull the latest version from your org.
- Deploy: Right-click and choose SFDX: Deploy Source to Org to push your local changes to the org.
- Save-on-deploy: You can also enable automatic deployment on save, so every time you save a file, it is pushed to the org immediately.
Installing and Setting Up IntelliJ with Illuminated Cloud 2
IntelliJ IDEA is a powerful IDE from JetBrains, and Illuminated Cloud 2 is a paid plugin that adds Salesforce development support.
Why Choose IntelliJ?
- Superior code navigation — IntelliJ’s indexing and navigation capabilities are more advanced than VS Code for large projects.
- Richer refactoring tools — Rename, extract method, inline variable, and other refactoring operations work across your entire project.
- Integrated testing — Run and debug Apex tests directly from the IDE with detailed results.
Setup Steps
- Download and install IntelliJ IDEA (Community or Ultimate edition) from jetbrains.com.
- Install the Illuminated Cloud 2 plugin from the JetBrains Marketplace (Preferences > Plugins > Marketplace > search “Illuminated Cloud 2”).
- Activate your Illuminated Cloud 2 license (it requires a paid subscription after the trial period).
- Create a new Salesforce project or open an existing Salesforce DX project.
- Configure the connection to your Developer Edition org through the plugin settings.
VS Code vs IntelliJ: Quick Comparison
| Feature | VS Code | IntelliJ + IC2 |
|---|---|---|
| Cost | Free | IntelliJ Community is free; IC2 requires a paid license |
| Setup complexity | Simple | More involved |
| Code navigation | Good | Excellent |
| Refactoring | Basic | Advanced |
| Salesforce CLI integration | Native | Through plugin |
| Community support | Very large | Smaller but dedicated |
| Performance on large projects | Can slow down | Generally better |
For beginners, VS Code is the recommended starting point. It is free, easier to set up, and has the largest community. As your projects grow in complexity, IntelliJ with Illuminated Cloud 2 becomes worth evaluating.
What is an Apex Class?
An Apex class is a blueprint that defines a set of variables (data) and methods (behavior). Classes are the fundamental building blocks of Apex development.
File Structure
Every Apex class consists of:
- An access modifier (
public,private,global). - The
classkeyword. - The class name.
- A pair of curly braces enclosing the class body.
public class AccountService {
// Variables go here
// Methods go here
}
When you create an Apex class in Salesforce, two files are generated:
AccountService.cls— Contains the class code.AccountService.cls-meta.xml— Contains metadata about the class (API version, status).
Naming Conventions
- Use PascalCase for class names:
AccountService,ContactHelper,OpportunityTriggerHandler. - Class names should be descriptive and indicate the class’s purpose.
- Avoid generic names like
MyClassorTest1in production code. - Common suffixes include
Service,Helper,Handler,Controller,Selector,Util, andFactory.
What Are Variables?
A variable is a named storage location that holds a value. In Apex, every variable has a type, a name, and an optional initial value.
Declaration and Initialization
// Declaration only
String accountName;
// Declaration with initialization
String accountName = 'Acme Corporation';
// Multiple declarations of the same type
Integer count = 0;
Integer maxRetries = 3;
// Declare now, assign later
Decimal totalRevenue;
totalRevenue = 1500000.75;
Naming Conventions
- Use camelCase for variable names:
accountName,totalRevenue,isActive. - Start with a lowercase letter.
- Be descriptive:
numberOfContactsis better thannornum. - Boolean variables often start with
is,has, orshould:isActive,hasPermission,shouldSendEmail.
Primitive Data Types
Primitive data types are the basic building blocks. They hold simple values and are not objects.
Integer
A 32-bit whole number with no decimal point.
Integer count = 42;
Integer negativeNumber = -10;
Integer maxValue = 2147483647; // Maximum Integer value
Long
A 64-bit whole number. Use it when Integer is not large enough.
Long bigNumber = 2147483648L;
Long recordCount = 9999999999L;
Double
A 64-bit floating-point number. Use it for decimal values where precision is not critical.
Double pi = 3.14159;
Double temperature = -40.5;
Decimal
An arbitrary-precision signed decimal number. Use it for financial calculations where precision matters.
Decimal price = 29.99;
Decimal taxRate = 0.0825;
Decimal total = price * (1 + taxRate); // 32.4639...
// Set scale (decimal places)
Decimal rounded = total.setScale(2); // 32.46
Decimal is preferred over Double for currency and financial calculations because it avoids floating-point rounding errors.
String
A sequence of characters enclosed in single quotes.
String greeting = 'Hello, World!';
String empty = '';
String nullString = null;
// Common String methods
Integer len = greeting.length(); // 13
String upper = greeting.toUpperCase(); // 'HELLO, WORLD!'
String lower = greeting.toLowerCase(); // 'hello, world!'
Boolean starts = greeting.startsWith('Hello'); // true
Boolean contains = greeting.contains('World'); // true
String sub = greeting.substring(0, 5); // 'Hello'
String trimmed = ' spaces '.trim(); // 'spaces'
Note that Apex uses single quotes for strings, not double quotes.
Boolean
A value that is either true or false.
Boolean isActive = true;
Boolean hasErrors = false;
Boolean isNull = null; // Booleans can be null
Date
A value that represents a calendar date (year, month, day) without a time component.
Date today = Date.today();
Date specific = Date.newInstance(2026, 3, 27);
Date tomorrow = today.addDays(1);
Date nextMonth = today.addMonths(1);
Integer dayOfMonth = today.day();
Integer month = today.month();
Integer year = today.year();
Integer daysBetween = today.daysBetween(specific);
Datetime
A value that represents a date and time combined.
Datetime now = Datetime.now();
Datetime specific = Datetime.newInstance(2026, 3, 27, 14, 30, 0);
Date dateOnly = now.date();
Time timeOnly = now.time();
String formatted = now.format('yyyy-MM-dd HH:mm:ss');
Time
A value that represents a time of day (hours, minutes, seconds, milliseconds).
Time t = Time.newInstance(14, 30, 0, 0); // 2:30 PM
Integer hour = t.hour(); // 14
Integer minute = t.minute(); // 30
Id
An 18-character Salesforce record identifier. Apex automatically validates that the value is a properly formatted Salesforce ID.
Id accountId = '001B000001LwiKHIAZ';
Id contactId = someContact.Id;
// You can assign a 15-character ID and Apex converts it to 18 characters
Id shortId = '001B000001LwiKH';
Blob
A collection of binary data. Used for file attachments, encoding, and cryptographic operations.
Blob fileBody = Blob.valueOf('Hello');
String encoded = EncodingUtil.base64Encode(fileBody);
Blob decoded = EncodingUtil.base64Decode(encoded);
Complex / Non-Primitive Data Types
Beyond primitives, Apex supports several complex data types.
sObjects
An sObject is a generic Salesforce object — any standard or custom object record. Each specific object (Account, Contact, My_Custom_Object__c) is a subtype of sObject.
// Generic sObject
sObject genericRecord = new Account(Name = 'Acme');
// Specific sObject types
Account acc = new Account();
acc.Name = 'Acme Corporation';
acc.Industry = 'Technology';
Contact con = new Contact(
FirstName = 'John',
LastName = 'Doe',
Email = 'john.doe@example.com'
);
Enums
An enum is a fixed set of named constants. Use enums when a variable should only hold one of a predefined set of values.
public enum Season {
SPRING,
SUMMER,
AUTUMN,
WINTER
}
// Usage
Season current = Season.SUMMER;
if (current == Season.SUMMER) {
System.debug('It is summer.');
}
Salesforce also provides built-in enums like System.StatusCode, DisplayType, and TriggerOperation.
Collections
Collections are data structures that hold multiple values. Apex has three collection types — Lists, Sets, and Maps — which we will cover in detail later in this post.
Custom Classes
Any class you define is itself a data type. You can create variables whose type is your custom class.
public class Address {
public String street;
public String city;
public String state;
public String zipCode;
}
// Usage
Address homeAddress = new Address();
homeAddress.street = '123 Main St';
homeAddress.city = 'San Francisco';
homeAddress.state = 'CA';
homeAddress.zipCode = '94105';
Variable Scope
Variable scope determines where a variable can be accessed in your code. Apex has three levels of scope.
Class-Level Scope
Variables declared directly inside a class (but outside any method) are accessible from any method in that class. These are also called instance variables or member variables.
public class OrderProcessor {
// Class-level variable — accessible throughout the class
private String orderStatus = 'New';
private Integer itemCount = 0;
public void processOrder() {
// Can access orderStatus and itemCount here
orderStatus = 'Processing';
itemCount = 5;
}
public String getStatus() {
// Can also access orderStatus here
return orderStatus;
}
}
Method-Level Scope
Variables declared inside a method are only accessible within that method. They are created when the method starts and destroyed when it ends.
public class Calculator {
public Integer add(Integer a, Integer b) {
// result only exists inside this method
Integer result = a + b;
return result;
}
public void anotherMethod() {
// This would cause a compile error:
// System.debug(result); // result is not accessible here
}
}
Block-Level Scope
Variables declared inside a block (such as an if statement, for loop, or while loop) are only accessible within that block.
public class ScopeExample {
public void demonstrate() {
Integer outerVar = 10;
if (outerVar > 5) {
// blockVar only exists inside this if block
String blockVar = 'Inside the block';
System.debug(blockVar); // Works fine
}
// This would cause a compile error:
// System.debug(blockVar); // blockVar is not accessible here
for (Integer i = 0; i < 3; i++) {
// i only exists inside this for loop
System.debug('Loop index: ' + i);
}
// This would cause a compile error:
// System.debug(i); // i is not accessible here
}
}
The rule is simple: a variable is accessible within the curly braces {} where it is declared, and in any nested blocks within those braces.
What Are Methods?
A method is a block of code that performs a specific task. Methods are defined inside a class and can accept input (parameters), execute logic, and return output.
Method Structure
accessModifier returnType methodName(parameterType parameterName) {
// Method body
return value; // if returnType is not void
}
Examples
public class MathHelper {
// Method that takes two integers and returns their sum
public Integer add(Integer a, Integer b) {
return a + b;
}
// Method that takes no parameters and returns a String
public String getGreeting() {
return 'Hello from MathHelper!';
}
// Void method — performs an action but returns nothing
public void logMessage(String message) {
System.debug('LOG: ' + message);
}
// Method with multiple parameters
public Decimal calculateDiscount(Decimal price, Decimal discountPercent) {
Decimal discount = price * (discountPercent / 100);
return price - discount;
}
}
Method Signatures
A method signature is the combination of the method name and its parameter list. Apex supports method overloading, which means you can have multiple methods with the same name as long as their parameter lists differ.
public class Formatter {
public String format(Integer value) {
return String.valueOf(value);
}
public String format(Decimal value) {
return value.setScale(2).toPlainString();
}
public String format(Date value) {
return value.format();
}
}
All three methods are named format, but they accept different parameter types. Apex determines which method to call based on the argument type you pass.
What is a Constructor?
A constructor is a special method that runs when you create a new instance of a class using the new keyword. Constructors have the same name as the class and do not have a return type.
Default Constructor
If you do not define any constructor, Apex provides a default no-argument constructor automatically. Once you define any constructor, the default constructor is no longer provided — you must define it explicitly if you still need it.
public class TaskManager {
public String taskName;
public String priority;
// Default (no-argument) constructor
public TaskManager() {
taskName = 'Untitled';
priority = 'Medium';
}
}
// Usage
TaskManager tm = new TaskManager();
System.debug(tm.taskName); // 'Untitled'
System.debug(tm.priority); // 'Medium'
Parameterized Constructor
A parameterized constructor accepts arguments, allowing you to initialize an object with specific values at the time of creation.
public class TaskManager {
public String taskName;
public String priority;
// No-argument constructor
public TaskManager() {
taskName = 'Untitled';
priority = 'Medium';
}
// Parameterized constructor
public TaskManager(String name, String prio) {
taskName = name;
priority = prio;
}
}
// Usage
TaskManager defaultTask = new TaskManager();
TaskManager specificTask = new TaskManager('Follow Up', 'High');
System.debug(specificTask.taskName); // 'Follow Up'
System.debug(specificTask.priority); // 'High'
Constructor Chaining
Constructor chaining is the practice of having one constructor call another using the this() keyword. This avoids duplicating initialization logic.
public class TaskManager {
public String taskName;
public String priority;
public Date dueDate;
public TaskManager() {
this('Untitled', 'Medium');
}
public TaskManager(String name, String prio) {
this(name, prio, Date.today().addDays(7));
}
public TaskManager(String name, String prio, Date due) {
taskName = name;
priority = prio;
dueDate = due;
}
}
In this example, calling new TaskManager() chains through all three constructors, ultimately setting all three fields with default values.
How to Instantiate a Class
Instantiation means creating a new object from a class using the new keyword. When you instantiate a class, Apex allocates memory for the object and calls the constructor.
// Using the no-argument constructor
TaskManager tm1 = new TaskManager();
// Using a parameterized constructor
TaskManager tm2 = new TaskManager('Deploy Code', 'High');
// Using a constructor with all parameters
TaskManager tm3 = new TaskManager('Write Tests', 'Medium', Date.newInstance(2026, 4, 15));
You can also instantiate classes and immediately call methods on them:
String result = new Formatter().format(42);
And you can store references and pass them to other methods:
TaskManager task = new TaskManager('Review PR', 'High');
processTask(task);
Operators in Apex
Operators perform operations on variables and values. Apex supports a comprehensive set of operators.
Arithmetic Operators
Integer a = 10;
Integer b = 3;
Integer sum = a + b; // 13
Integer difference = a - b; // 7
Integer product = a * b; // 30
Integer quotient = a / b; // 3 (integer division, truncates)
Integer remainder = Math.mod(a, b); // 1
Note that Apex does not have a % modulus operator. Use Math.mod() instead.
String Concatenation
The + operator concatenates strings:
String first = 'Hello';
String second = 'World';
String combined = first + ', ' + second + '!'; // 'Hello, World!'
Assignment Operators
Integer x = 10; // Simple assignment
x += 5; // x = x + 5 → 15
x -= 3; // x = x - 3 → 12
x *= 2; // x = x * 2 → 24
x /= 4; // x = x / 4 → 6
Comparison Operators
Integer a = 10;
Integer b = 20;
Boolean result1 = (a == b); // false — equal to
Boolean result2 = (a != b); // true — not equal to
Boolean result3 = (a < b); // true — less than
Boolean result4 = (a > b); // false — greater than
Boolean result5 = (a <= b); // true — less than or equal to
Boolean result6 = (a >= b); // false — greater than or equal to
For strings, use == for case-insensitive comparison and .equals() for case-sensitive comparison:
String s1 = 'Hello';
String s2 = 'hello';
Boolean caseInsensitive = (s1 == s2); // true
Boolean caseSensitive = s1.equals(s2); // false
Logical Operators
Boolean a = true;
Boolean b = false;
Boolean andResult = a && b; // false — both must be true
Boolean orResult = a || b; // true — at least one must be true
Boolean notResult = !a; // false — negation
Ternary Operator
A shorthand for if-else that returns a value:
Integer score = 85;
String grade = (score >= 90) ? 'A' : 'B';
// grade = 'B' because 85 is not >= 90
String name = null;
String displayName = (name != null) ? name : 'Unknown';
// displayName = 'Unknown'
instanceof Operator
Checks whether an object is an instance of a particular class or interface:
sObject record = new Account(Name = 'Test');
if (record instanceof Account) {
Account acc = (Account) record;
System.debug('This is an Account: ' + acc.Name);
}
Safe Navigation Operator (?.)
The safe navigation operator prevents null pointer exceptions by short-circuiting to null when the left side of the expression is null:
// Without safe navigation — throws NullPointerException if acc is null
// String name = acc.Name;
// With safe navigation — returns null if acc is null
Account acc = null;
String name = acc?.Name; // name is null, no exception thrown
// Chaining safe navigation
String cityName = acc?.BillingAddress?.getCity();
This operator is particularly useful when navigating relationships that might not exist.
What Are Collections?
Collections are data structures that store multiple elements. Apex provides three collection types:
| Collection | Description | Ordered? | Allows Duplicates? | Access By |
|---|---|---|---|---|
| List | An ordered collection of elements | Yes | Yes | Index (0-based) |
| Set | An unordered collection of unique elements | No | No | N/A (membership check) |
| Map | A collection of key-value pairs | No* | Keys: No, Values: Yes | Key |
*Maps maintain insertion order when iterated, but you should not rely on this behavior.
Lists
A List is an ordered collection of elements accessed by a zero-based index. Lists are the most commonly used collection in Apex.
Declaration and Initialization
// Declare an empty list
List<String> names = new List<String>();
// Declare and initialize with values
List<String> colors = new List<String>{'Red', 'Green', 'Blue'};
// List of Integers
List<Integer> numbers = new List<Integer>{1, 2, 3, 4, 5};
// List of sObjects
List<Account> accounts = new List<Account>();
// Alternative array syntax (works the same way)
String[] fruits = new String[]{'Apple', 'Banana', 'Cherry'};
Common Methods
List<String> cities = new List<String>();
// add — Append an element to the end
cities.add('New York');
cities.add('London');
cities.add('Tokyo');
// add at index — Insert at a specific position
cities.add(1, 'Paris'); // List is now: New York, Paris, London, Tokyo
// get — Retrieve an element by index
String first = cities.get(0); // 'New York'
String second = cities[1]; // 'Paris' (bracket notation also works)
// set — Replace an element at a specific index
cities.set(2, 'Berlin'); // London is replaced with Berlin
// remove — Remove an element by index
cities.remove(0); // Removes 'New York'
// size — Get the number of elements
Integer count = cities.size(); // 3
// contains — Check if an element exists
Boolean hasTokyo = cities.contains('Tokyo'); // true
// isEmpty — Check if the list is empty
Boolean empty = cities.isEmpty(); // false
// sort — Sort elements in ascending order
cities.sort(); // Alphabetical order: Berlin, Paris, Tokyo
// clear — Remove all elements
cities.clear();
Iterating Over a List
List<String> fruits = new List<String>{'Apple', 'Banana', 'Cherry'};
// Standard for loop
for (Integer i = 0; i < fruits.size(); i++) {
System.debug('Index ' + i + ': ' + fruits[i]);
}
// Enhanced for loop (for-each)
for (String fruit : fruits) {
System.debug('Fruit: ' + fruit);
}
Sets
A Set is an unordered collection of unique elements. If you add a duplicate, it is silently ignored.
Declaration and Common Methods
// Declare an empty set
Set<String> tags = new Set<String>();
// Declare and initialize
Set<String> statuses = new Set<String>{'New', 'In Progress', 'Closed'};
// add — Add an element (duplicates are ignored)
tags.add('Apex');
tags.add('Trigger');
tags.add('Apex'); // Ignored — already exists
System.debug(tags.size()); // 2
// contains — Check membership
Boolean hasApex = tags.contains('Apex'); // true
// remove — Remove an element
tags.remove('Trigger');
// addAll — Add all elements from another collection
Set<String> moreTags = new Set<String>{'Flow', 'LWC'};
tags.addAll(moreTags);
// isEmpty
Boolean empty = tags.isEmpty(); // false
// Iteration
for (String tag : tags) {
System.debug('Tag: ' + tag);
}
When to Use a Set
- When you need to ensure uniqueness — for example, collecting a set of record IDs.
- When you need to perform fast membership checks —
Set.contains()is faster thanList.contains()for large collections. - When working with SOQL WHERE IN clauses — you can pass a Set directly into a query.
Set<Id> accountIds = new Set<Id>();
for (Contact con : contactList) {
accountIds.add(con.AccountId);
}
// Use the Set in a SOQL query
List<Account> accounts = [SELECT Id, Name FROM Account WHERE Id IN :accountIds];
Maps
A Map is a collection of key-value pairs. Each key maps to exactly one value. Keys must be unique; values can be duplicated.
Declaration and Common Methods
// Declare an empty map
Map<String, Integer> wordCounts = new Map<String, Integer>();
// Declare and initialize
Map<String, String> stateAbbreviations = new Map<String, String>{
'California' => 'CA',
'New York' => 'NY',
'Texas' => 'TX'
};
// put — Add or update a key-value pair
wordCounts.put('hello', 5);
wordCounts.put('world', 3);
wordCounts.put('hello', 7); // Updates the value for 'hello' to 7
// get — Retrieve a value by key
Integer count = wordCounts.get('hello'); // 7
Integer missing = wordCounts.get('foo'); // null (key does not exist)
// containsKey — Check if a key exists
Boolean hasHello = wordCounts.containsKey('hello'); // true
// keySet — Get all keys as a Set
Set<String> keys = wordCounts.keySet(); // {'hello', 'world'}
// values — Get all values as a List
List<Integer> vals = wordCounts.values(); // [7, 3]
// size — Get the number of key-value pairs
Integer mapSize = wordCounts.size(); // 2
// remove — Remove a key-value pair
wordCounts.remove('world');
The Map<Id, sObject> Pattern
This is one of the most important patterns in Apex development. You will use it constantly when working with triggers and bulk data processing.
// Build a map of Account ID to Account record
Map<Id, Account> accountMap = new Map<Id, Account>(
[SELECT Id, Name, Industry FROM Account WHERE Industry = 'Technology']
);
// Now you can look up any account by its ID in constant time
Account acme = accountMap.get('001B000001LwiKHIAZ');
// This pattern is essential in triggers
// For example, in a before update trigger:
Map<Id, Account> oldAccountMap = new Map<Id, Account>(Trigger.old);
for (Account newAcc : Trigger.new) {
Account oldAcc = oldAccountMap.get(newAcc.Id);
if (newAcc.Industry != oldAcc.Industry) {
System.debug('Industry changed for: ' + newAcc.Name);
}
}
The constructor new Map<Id, sObject>(listOfRecords) automatically builds the map using the record IDs as keys. This is a Salesforce-specific convenience that you will rely on heavily.
Iterating Over a Map
Map<String, Integer> scores = new Map<String, Integer>{
'Alice' => 95,
'Bob' => 87,
'Carol' => 92
};
// Iterate over keys
for (String name : scores.keySet()) {
System.debug(name + ': ' + scores.get(name));
}
// Iterate over values only
for (Integer score : scores.values()) {
System.debug('Score: ' + score);
}
The Static Keyword
The static keyword in Apex means that a variable or method belongs to the class itself rather than to any specific instance of the class. You do not need to create an object to access static members.
Static Variables
A static variable is shared across all instances of a class within a single transaction. There is only one copy of a static variable, no matter how many instances you create.
public class Counter {
public static Integer count = 0;
public Counter() {
count++;
}
}
// Usage
Counter c1 = new Counter();
Counter c2 = new Counter();
Counter c3 = new Counter();
System.debug(Counter.count); // 3
Notice that you access static variables using the class name, not an instance variable.
Static Methods
A static method can be called without creating an instance of the class. Static methods can only access other static members — they cannot access instance variables or instance methods directly.
public class MathUtils {
public static Integer square(Integer num) {
return num * num;
}
public static Decimal average(List<Decimal> numbers) {
if (numbers == null || numbers.isEmpty()) {
return 0;
}
Decimal total = 0;
for (Decimal n : numbers) {
total += n;
}
return total / numbers.size();
}
}
// Usage — no need to create an instance
Integer result = MathUtils.square(5); // 25
Decimal avg = MathUtils.average(new List<Decimal>{10, 20, 30}); // 20
Static Initialization Blocks
A static initialization block runs once when the class is first loaded. Use it to initialize complex static variables.
public class Config {
public static Map<String, String> settings;
static {
settings = new Map<String, String>();
settings.put('MAX_RETRIES', '3');
settings.put('TIMEOUT', '30000');
settings.put('LOG_LEVEL', 'DEBUG');
}
}
// Usage
String maxRetries = Config.settings.get('MAX_RETRIES'); // '3'
When to Use Static
- Utility methods that do not depend on instance state (e.g., math helpers, formatting utilities).
- Constants that should be shared across all instances.
- Counters and flags that track state across a transaction (e.g., preventing recursive trigger execution).
- Factory methods that create and return instances of the class.
Static vs. Instance
Understanding the difference between static and instance members is fundamental.
| Aspect | Static | Instance |
|---|---|---|
| Belongs to | The class | A specific object |
| Access syntax | ClassName.member | objectVariable.member |
| Memory | One copy per transaction | One copy per object |
| Can access | Only other static members | Both static and instance members |
| Created when | Class is first referenced | Object is instantiated with new |
| Use case | Shared utilities, constants, counters | Object-specific data and behavior |
Example Showing the Difference
public class Employee {
// Static variable — shared across all employees
public static Integer totalEmployees = 0;
// Instance variables — unique to each employee
public String name;
public String department;
// Constructor
public Employee(String name, String department) {
this.name = name;
this.department = department;
totalEmployees++;
}
// Static method — operates on class-level data
public static Integer getHeadcount() {
return totalEmployees;
}
// Instance method — operates on this specific employee's data
public String getDetails() {
return name + ' (' + department + ')';
}
}
// Usage
Employee e1 = new Employee('Alice', 'Engineering');
Employee e2 = new Employee('Bob', 'Marketing');
System.debug(e1.getDetails()); // 'Alice (Engineering)'
System.debug(e2.getDetails()); // 'Bob (Marketing)'
System.debug(Employee.getHeadcount()); // 2
System.debug(Employee.totalEmployees); // 2
A common mistake is trying to access instance members from a static method. This will not compile:
public class Broken {
public String name = 'Test';
public static void printName() {
// COMPILE ERROR: non-static variable cannot be referenced from a static context
System.debug(name);
}
}
To fix this, either make the variable static or pass an instance to the method as a parameter.
Access Modifiers
Access modifiers control the visibility of classes, methods, and variables. Apex has four access modifiers.
private
The member is only accessible within the class where it is defined. This is the default access level — if you do not specify an access modifier, the member is private.
public class BankAccount {
private Decimal balance = 0;
public void deposit(Decimal amount) {
// Private members are accessible within the same class
balance += amount;
}
public Decimal getBalance() {
return balance;
}
}
// Usage
BankAccount account = new BankAccount();
account.deposit(500);
System.debug(account.getBalance()); // 500
// This would cause a compile error:
// System.debug(account.balance); // balance is private
public
The member is accessible from any Apex code within the same namespace (your org). Most of the code you write will use public.
public class ContactService {
public String serviceName = 'Contact Service';
public List<Contact> getContactsByAccount(Id accountId) {
return [SELECT Id, Name FROM Contact WHERE AccountId = :accountId];
}
}
// Accessible from any other class in the org
ContactService svc = new ContactService();
List<Contact> contacts = svc.getContactsByAccount(someAccountId);
protected
The member is accessible within the class where it is defined and within any class that extends it (subclasses). Protected is used in inheritance scenarios.
public virtual class BaseProcessor {
protected String processorType = 'Base';
protected void log(String message) {
System.debug(processorType + ': ' + message);
}
}
public class OrderProcessor extends BaseProcessor {
public OrderProcessor() {
// Can access protected members from the parent class
processorType = 'Order';
}
public void process() {
// Can call protected methods from the parent class
log('Processing order...');
}
}
global
The member is accessible from any Apex code, including code in other namespaces (managed packages). Use global only when you are building managed packages or exposing web services and REST endpoints.
global class MyWebService {
webService static String getStatus() {
return 'OK';
}
}
Visibility Rules Summary
| Modifier | Same Class | Same Namespace | Subclass | Other Namespaces |
|---|---|---|---|---|
private | Yes | No | No | No |
protected | Yes | No | Yes | No |
public | Yes | Yes | Yes | No |
global | Yes | Yes | Yes | Yes |
Best Practices
- Default to
private: Keep variables private and expose them through public methods (getters and setters). This is the principle of encapsulation. - Use
publicfor methods that other classes need to call. - Use
protectedsparingly — only when you have a clear inheritance hierarchy. - Avoid
globalunless necessary — once a member is global, you cannot reduce its visibility without breaking dependent code. This is especially important in managed packages.
Section Notes
Here are the key takeaways from this post:
- Apex is a strongly typed, object-oriented language that runs on Salesforce servers and is subject to governor limits. If you know Java or C#, the syntax will feel familiar.
- Set up your development environment with VS Code and the Salesforce Extension Pack. It is free, well-supported, and the officially recommended option.
- Classes are blueprints that contain variables and methods. Follow naming conventions — PascalCase for classes, camelCase for variables and methods.
- Know your data types — use
Decimalfor currency,Idfor record identifiers, and understand the difference between primitive and non-primitive types. - Variable scope matters — a variable is only accessible within the block where it is declared. Keep scope as narrow as possible.
- Methods define behavior and can be overloaded. Constructors are special methods that initialize objects.
- Collections are essential — Lists for ordered data, Sets for uniqueness, Maps for key-value lookups. The
Map<Id, sObject>pattern is one of the most important tools in your Apex toolkit. - Static members belong to the class, not to instances. Use static for utilities, constants, and transaction-level state.
- Access modifiers control visibility — default to
private, usepublicfor APIs, avoidglobalunless you are building a managed package.
PROJECT: Create an Apex Class
Let us put everything together in a hands-on exercise. You will create an Apex class that uses variables, methods, constructors, collections, access modifiers, and static members.
The Goal
Build a StudentManager class that manages a list of students, tracks enrollments, and provides utility methods.
Step 1: Create the Student Class
Open VS Code and create a new Apex class. In the Command Palette, type SFDX: Create Apex Class, name it Student, and paste the following code:
public class Student {
// Private instance variables
private String name;
private String email;
private Integer age;
private List<String> courses;
// Parameterized constructor
public Student(String name, String email, Integer age) {
this.name = name;
this.email = email;
this.age = age;
this.courses = new List<String>();
}
// Public getter methods
public String getName() {
return name;
}
public String getEmail() {
return email;
}
public Integer getAge() {
return age;
}
public List<String> getCourses() {
return courses;
}
// Enroll in a course
public void enroll(String courseName) {
if (!courses.contains(courseName)) {
courses.add(courseName);
}
}
// Drop a course
public void drop(String courseName) {
courses.remove(courses.indexOf(courseName));
}
// Get a summary string
public String getSummary() {
return name + ' (' + email + ') — ' + courses.size() + ' course(s)';
}
}
Step 2: Create the StudentManager Class
Create another Apex class named StudentManager:
public class StudentManager {
// Static variable to track total enrollments across all managers
private static Integer totalEnrollments = 0;
// Instance variables
private List<Student> students;
private String managerName;
// No-argument constructor
public StudentManager() {
this('Default Manager');
}
// Parameterized constructor (constructor chaining)
public StudentManager(String managerName) {
this.managerName = managerName;
this.students = new List<Student>();
}
// Add a student
public void addStudent(Student s) {
students.add(s);
totalEnrollments++;
}
// Get all student names as a Set (ensures uniqueness)
public Set<String> getStudentNames() {
Set<String> names = new Set<String>();
for (Student s : students) {
names.add(s.getName());
}
return names;
}
// Build a Map of student email to Student object
public Map<String, Student> getStudentsByEmail() {
Map<String, Student> emailMap = new Map<String, Student>();
for (Student s : students) {
emailMap.put(s.getEmail(), s);
}
return emailMap;
}
// Get students enrolled in a specific course
public List<Student> getStudentsByCourse(String courseName) {
List<Student> result = new List<Student>();
for (Student s : students) {
if (s.getCourses().contains(courseName)) {
result.add(s);
}
}
return result;
}
// Static method — accessible without an instance
public static Integer getTotalEnrollments() {
return totalEnrollments;
}
// Get the number of students in this manager
public Integer getStudentCount() {
return students.size();
}
// Print a roster to the debug log
public void printRoster() {
System.debug('=== Roster for ' + managerName + ' ===');
for (Student s : students) {
System.debug(s.getSummary());
}
System.debug('Total students: ' + students.size());
}
}
Step 3: Test It in the Developer Console
Open the Developer Console from your org (click the gear icon > Developer Console). Open the Execute Anonymous window (Debug > Open Execute Anonymous Window) and run:
// Create students
Student alice = new Student('Alice Johnson', 'alice@example.com', 20);
Student bob = new Student('Bob Smith', 'bob@example.com', 22);
Student carol = new Student('Carol Lee', 'carol@example.com', 21);
// Enroll in courses
alice.enroll('Apex Basics');
alice.enroll('Lightning Web Components');
bob.enroll('Apex Basics');
bob.enroll('Admin Fundamentals');
carol.enroll('Lightning Web Components');
carol.enroll('Integration Patterns');
// Create a StudentManager and add students
StudentManager manager = new StudentManager('Spring 2026 Cohort');
manager.addStudent(alice);
manager.addStudent(bob);
manager.addStudent(carol);
// Print the roster
manager.printRoster();
// Use collections
Set<String> names = manager.getStudentNames();
System.debug('All names: ' + names);
Map<String, Student> emailMap = manager.getStudentsByEmail();
Student found = emailMap.get('bob@example.com');
System.debug('Found student: ' + found.getSummary());
List<Student> apexStudents = manager.getStudentsByCourse('Apex Basics');
System.debug('Students in Apex Basics: ' + apexStudents.size());
// Static method
System.debug('Total enrollments across all managers: ' + StudentManager.getTotalEnrollments());
What This Project Covers
- Classes — Both
StudentandStudentManagerare custom Apex classes. - Variables — Private instance variables with public getters (encapsulation).
- Primitive data types —
String,Integer. - Collections —
List<Student>,Set<String>,Map<String, Student>, andList<String>for courses. - Methods — Multiple methods with different return types and parameters.
- Constructors — No-argument and parameterized constructors with constructor chaining.
- Access modifiers —
privatefor variables,publicfor methods and classes. - Static keyword —
totalEnrollmentsandgetTotalEnrollments()demonstrate static vs. instance behavior. - Variable scope — Method-level variables like
resultandnamesare scoped to their methods.
Deploy both classes to your org and verify they work by running the anonymous script above.
What Comes Next
In Part 38: Conditional Statements and Collection Iteration in Apex, we will cover if-else chains, switch statements, for loops, while loops, do-while loops, and how to iterate over Lists, Sets, and Maps effectively. These control flow structures are the glue that connects the building blocks you learned today into real business logic. See you there.