Salesforce is a popular CRM platform that most organizations use to enhance their customer relationships and business productivity.
Salesforce reports that companies across the automotive, communications, education, financial services, and manufacturing sectors use Salesforce to modernize their businesses and reduce costs.
This tutorial helps you learn about the Apex programming language and how to develop Salesforce applications with it.
Table of Contents:
Apex is the programming language which Salesforce designed for their platform. Since it is object-oriented as well as strongly typed and runs server-side, your code would execute on Salesforce's infrastructure.
It means that your Apex code can talk directly to the database, access the logged-in user's info, read org metadata. It can do all of these without configuring connection strings or standing up middleware. It just works out of the gate, which is honestly one of the nicer things about developing on this platform.
Anyone with a Java background will pick this up quickly.
The syntax is very similar. Classes, methods, instance variables, loops, try-catch you'll recognize all of it.
However, it is different in the runtime environment. Salesforce is multi-tenant, meaning your organisation can share server resources with other organisations. Hence, they have put strong limitations on what your code can do within a single execution. It includes elements like
These caps have the name "governor limits," and the whole reason they exist is to stop one user’s weakly optimized code from dragging down performance for others on the same server cluster.
Talking about the data type, Apex includes:
So, if you have used any C-based language, you would feel comfortable with these.
The thing that will feel new, and this is genuinely cool if you're coming from Java or Python, is sObjects.
An sObject is a direct and in-memory representation of an actual Salesforce record. So, when you type Account acc = new Account(); and start setting fields on it, you are building a database row in code. Call insert acc; and that object becomes real data sitting in your org. The first time you do that, it will click immediately why people like building on this platform.
Variable declarations look like this in practice:
apex
Integer headcount = 120;
String companyName = 'Zenith Solutions';
Boolean isVerified = true;
Double totalRevenue = 3400000.50;
Date joinDate = Date.newInstance(2022, 6, 10);
And here's how you work with an sObject:
apex
Account newAccount = new Account();
newAccount. Name = 'Orion Technologies';
newAccount. Industry = 'Healthcare';
newAccount.AnnualRevenue = 5000000;
That Account object is ready to be inserted into the database whenever you run a DML insert statement.
The easiest and quickest way to run Apex code is through the Developer Console.
apex
String msg = 'First line of Apex - feels good.';
System.debug(msg);
Integer x = 8;
Integer y = 13;
System.debug('Total: '+ (x + y));
System.debug() is how you print output in Apex. There's no console.log or System.out.println here it's all System.debug(), and you read the results in the debug log.
That covers the basics. If you want a more thorough understanding of Apex fundamentals with hands-on labs, MindMajix's Salesforce Development training program covers this and a lot more.
Now let's see how you can control what your code does and when it does it.
Without control flow statements, your code would just run line by line from top to bottom i.e, every single line for every single time. But, that’s not how real efficient programs work.
You need your code to make decisions, i.e., should this discount apply or not? You need your code to repeat things and process every record in this list. That's what control flow gives you.
Apex supports two kinds of control flow.
This is the most basic conditional. You check something first. Based on whether it's true or false, different code runs.
Output: Standard shipping fee added
The condition was false, so the “else” block ran.
Most business logic isn't simple true/false. You usually have multiple possible outcomes, and you need to check for each one in order.
Output: Neither value set to -1
The important thing to remember is that these conditions are tested from top to bottom. The moment one of them is true, its block executes, and everything below it gets skipped. So the order you arrange your conditions in actually matters.
If you have a single variable and need to match it against other specific values, go for the switch statement. It is a cleaner option compared to writing multiple if-else blocks.
Output: Assign to senior engineer
This reads a lot better than a thread of if-else statements doing the same thing.
The while loop repeats a code block as long as a condition holds true. It checks the condition before each run. So if the condition is false from the start, the loop doesn’t execute at all.
Output: Prints values 1 through 5.
One thing to watch if you forget the j++ part, the condition never becomes false, and you've got yourself an infinite loop. Salesforce will kill it eventually when CPU limits are hit, but that's not how you want to discover the problem.
This works in a similar way to a while loop, except that the condition is checked after the code block runs. This means the code will surely execute at least once, irrespective of anything.
Output: Prints values from 5 to 14.
The first print happens before the condition e <= 14 is evaluated. That's the whole point when you need something to run at least once.
When you know exactly how many iterations you need, the for loop does initialization, condition checking, and increments to one neat line.
Output: Prints 0 through 9.
Three parts separated by semicolons i.e, declare your counter, set your stopping condition, and define how the counter changes after each pass.
This loop was designed for iterating through collections and arrays. Instead of managing an index variable yourself, Apex just hands you each element one at a time.
Output: Prints each element sequentially.
The Apex assigns the current element to the variable e while running the loop. But when it runs out of elements, the loop stops. Also, you don't have to worry about indexes being out of bounds or off-by-one mistakes.
So, if you want to start running from a particular statement based on a condition, conditional statements are important. And, if you want to execute code repeatedly, you can use loops. Together, these control flow tools help you handle any business logic scenario.
Next up are the data structures that hold all the information your code works with.

Collections are basically containers that hold multiple elements. You can think of them as smarter versions of arrays, which grow and shrink on their own. They come with built-in methods for searching and sorting, and they work seamlessly with Salesforce's database layer.
Salesforce gives you three types:
Let's check these with code examples.
A List is an ordered collection of elements. It means the elements retain the position you first place them in. Like, first one at index 0, second at index 1, and so on. Lists easily handle duplicates. So, if you add the same value ten times, it stores all ten.
Also, every SOQL query in Salesforce returns a List. This makes it the most-used collection type in all of Apex development.
List elements can be primitive types like String or Integer, sObjects like Account or Contact, user-defined classes, or even other collections. They automatically adjust the size as you add or remove items.
Output: (Mango, Apple, Banana, Apple). Here, both Apple entries are kept. Duplicates are perfectly fine in a List.
Adding Elements and Processing Them
Let's check out a scenario. Consider that you need to track employee salaries and run some operations on them.
Adding values with add():
Output: (45000, 62000, 78000, 51000)
Checking the size with size():
Output: 4
Grabbing a specific element with get():
Output: 62000
Remember indexes start at 0. So index 1 gives you the second element.
Removing an element with remove():
Output: (45000, 62000, 51000) the element at index 2 (78000) is gone.
Checking if a value exists with contains():
Output: true
Sorting the list with sort():
empSalary.sort();
System.debug('Sorted: ' + empSalary);
Output: (45000, 51000, 62000)
Checking if the list is empty with isEmpty():
System.debug('Empty? ' + empSalary.isEmpty());
Output: false
The for-each loop is the standard way to iterate through every element.
List<String> cities = new List<String>{'Hyderabad', 'Pune', 'Chennai', 'Delhi'};
for (String city : cities) {
System.debug('City: ' + city);
That's the List collection which is ordered, repeat-friendly, index-based. You'll use it in virtually every piece of Salesforce Development code you write.
Sets solve a very specific problem: they store elements without allowing any duplicates. You can try adding the same value a second time, and the Set just ignores it. You get no error message or exception. It simply doesn't add the duplicate.
The other thing about Sets is that they are unordered. There's no index. Here, you can't say "give me element number 3." You either check whether something is in the Set or you iterate through the whole thing.
Set elements can be primitive types (String, Integer, etc.), sObjects, or user-defined types.
Set<Integer> accountNumbers = new Set<Integer>();
Set<String> teams = new Set<String>{'Development', 'QA', 'DevOps'};
Here's a scenario straight from the MindMajix source material: adding employee bank account numbers and processing them with various Set operations.
Adding values:
Set<Integer> empBankAcc = new Set<Integer>();
empBankAcc.add(111);
empBankAcc.add(222);
empBankAcc.add(333);
empBankAcc.add(444);
System.debug('The Current Account Numbers = ' + empBankAcc);
Output: {111, 222, 333, 444}
Now try adding a duplicate:
empBankAcc.add(111);
System.debug('The Current Account Numbers = ' + empBankAcc);
Output: {111, 222, 333, 444}
An interesting thing here is that 111 was already in the Set. So, the second add(111) was completely ignored and the Set didn't expand. This is what makes Sets useful for de-duplication.
Checking whether a value exists:
Boolean checkTheValue = empBankAcc.contains(444);
System.debug('Does 444 exist? = ' + checkTheValue);
Output: true
Boolean checkAnother = empBankAcc.contains(999);
System.debug('Does 999 exist? = ' + checkAnother);
Output: false
Getting the size:
Integer length = empBankAcc.size();
System.debug('Total accounts = ' + length);
Output: 4
Checking if the Set is empty:
Boolean emptyCheck = empBankAcc.isEmpty();
System.debug('Is this set empty? = ' + emptyCheck);
Output: false
Removing a value:
empBankAcc.remove(222);
System.debug('After removing 222: ' + empBankAcc);
Output: {111, 333, 444}
Since there's no index, you use the for-each loop
Set<String> emailIds = new Set<String>{
'ravi@demo.com', 'priya@demo.com', 'kiran@demo.com'
};
for (String email : emailIds) {
System.debug('Email: ' + email);
}
Practice assignment: Create a set of 10 email IDs. Print each one using System.debug() inside a for-each loop. (Hint: SET + for-each loop.)
Sets
Maps are the most important collection type in real-world Salesforce Development.
A Map contains key-value pairs, where each key needs to be unique. You can not have two entries with the same key, but the values attached to those keys can repeat as many times as you need.
What makes Maps different from Lists? When it comes to a List, you look things up using an index number 0, 1, 2, 3, which doesn’t have a specific meaning. While, when you look things up in a Map, you use a meaningful key like a product name, an Account ID, a country code.
Some key characteristics from the source material:
Map<String, Integer> productStock = new Map<String, Integer>();
Map<String, String> countryCodes = new Map<String, String>{
'India' => 'I N',
'Germany' => 'DE',
'Brazil' => 'BR'
};
Building a Map - The Mobile Price Example
Adding key-value pairs with put():
Map<String, Integer> myMobilePrice = new Map<String, Integer>(); myMobilePrice.put('iphone x', 70000);
myMobilePrice.put('nokia', 60000);
myMobilePrice.put('samsung', 50000);
myMobilePrice.put('motorola', 40000);
System.debug('My Mobile prices List = ' + myMobilePrice);
Output: {iphone x=70000, motorola=40000, nokia=60000, samsung=50000}
Checking if the Map is empty:
Boolean emptyCheck = myMobilePrice.isEmpty();
System.debug('Is the map empty = ' + emptyCheck);
Output: false
Getting all keys with keySet():
Set<String> mobiles = myMobilePrice.keySet();
System.debug('List of Mobiles = ' + mobiles);
Output: {iphone x, motorola, nokia, samsung}
Notice something interesting? keySet() returns a Set. That makes sense; keys are always unique, just like Set elements. That's not a coincidence in the design.
Getting all values with values():
List<Integer> prices = myMobilePrice.values();
System.debug('List of Mobile Prices = ' + prices);
Output: (40000, 50000, 60000, 70000)
And values() returns a List. Again, it makes sense values can repeat, just like List elements.
Retrieving a specific value by key:
Integer nokiaPrice = myMobilePrice.get('nokia');
System.debug('Nokia price: ' + nokiaPrice);
Output: 60000
Checking if a key exists:
System.debug('Has samsung? ' + myMobilePrice.containsKey('samsung'));
Output: true
Getting the total count:
System.debug('Total brands: ' + myMobilePrice.size());
Output: 4
You can iterate over the key set and pull each value inside the loop body.
for (String brand : myMobilePrice.keySet()) {
Integer price = myMobilePrice.get(brand);
System.debug(brand + ' costs: ' + price);
}
This is where Maps prove their value more than anywhere else. In trigger development, you constantly need to access related records without running queries inside loops (which would hit governor limits almost immediately).
Here's the pattern that every serious Salesforce developer knows:
// Step 1 Collect unique IDs using a Set
Set<Id> accountIds = new Set<Id>();
for (Contact con : Trigger.new) {
if (con.AccountId != null) {
accountIds.add(con.AccountId);
}
}
// Step 2 Query once and dump results into a Map
Map<Id, Account> accountMap = new Map<Id, Account>(
[SELECT Id, Name, Industry FROM Account WHERE Id IN :accountIds]
);
// Step 3 Use the Map inside the loop. Zero additional queries.
for (Contact con : Trigger.new) {
if (accountMap.containsKey(con.AccountId)) {
Account parent = accountMap.get(con.AccountId);
System.debug(con.LastName + ' belongs to ' + parent.Name);
}
}
This pattern - Set for collecting IDs, Map for storing query results, for-each for processing, is the foundation of bulkification in Salesforce Development.
Write it on a sticky note and put it on your monitor to remember it often.
Practice assignment: Create a Map with 5 student names and their marks. Print every name-marks pair using a for-each loop.
| List | Set | Map | |
| Ordered? | Yes | No | No |
| Allows duplicates? | Yes | No | Keys: No / Values: Yes |
| How to access elements | By index get(0) | Only through iteration | By key get('keyName') |
| Nulls allowed? | Yes | Yes | Yes |
| Best for | SOQL results, ordered processing | Unique value collection, deduplication | Record lookups, trigger bulkification |
This one challenges users more and the difficult part is that the bug it causes can be hard to track down. While you change a variable in one way and somehow a completely different variable in a completely different part of your code changes too.
Has that ever happened to you? Well, then you might have been hit by a shallow copy.
Let's understand it properly.
When you use the assignment operator (=) to copy an sObject variable, Apex doesn't actually create a new object. It creates a new reference. This is basically a second name pointing to the same object in memory.
Here, both variables are looking at the same thing. Touch one, and the other feels it.
Output: acc1 Name: ChangedCorp acc2 Name: ChangedCorp
We only changed acc2, but acc1 changed right along with it. This is because the line acc2 = acc1 didn't copy any data; it just made acc2 point to the same object that acc1 was already pointing to.
The shallow copy just copies the handle, not the thing the handle is attached to.
Deep copy creates a totally new, completely independent object with all the same field values. While you modify one, the other is totally unaffected.
In Apex, you have to do this with the .clone() method.
Output: acc1 Name: OriginalCorp acc2 Name: IndependentCorp
Now they live in completely separate chunks of memory. Changing one has zero effect on the other. That's what deep copy gives you true independence.
The shallow-vs-deep issue gets worse with collections, because you might be accidentally sharing references to dozens or hundreds of records without realizing it.
The dangerous version shallow copy of a List:
Output: MUTATED
You only touched shallowCopy, but originalList got changed too. Both Lists contain references to the same Account objects in memory.
The safe version deep clone of a List:
Output: Original: MUTATED Deep copy: SAFE CHANGE
deepClone() creates new sObject instances for every element in the List. Each copy is completely independent.
| Shallow Copy | Deep Copy | |
| What gets copied | The reference only | The whole object all field values |
| Separate memory? | No both point to same object | Yes each has its own |
| Modifying one changes the other? | Yes, always | No, never |
| How to do it | = operator | .clone() for single records, .deepClone() for Lists |
| When to use | When you deliberately want shared access | When you need a truly independent copy |
You can use shallow copy when you are passing a record to a helper method which is supposed to modify the original. On the other hand, you use deep copy when you are creating backups, cloning records for insertion or if you want to make changes without side effects.
Understanding this difference will save you from a category of bugs which are genuinely hard to debug. The symptoms are subtle i.e. data changes when it should not, fields update when it should not and triggers behave inconsistently. 90% of the times, a shallow copy somewhere in the code poses an issue.
That covers a lot of ground. Let us recap what we walked through in this tutorial:
Apex Programming - this includes the language, the data types, sObjects, and how to run code in the Developer Console using System.debug().
Control Flow - this includes if-else for decisions, switch for multi-value matching, while and do-while for conditional repetition, for loops for counted iteration, and for-each loops for walking through collections.
List ordered collection, allows duplicates, accessed by index. Every SOQL query returns one of these, so you'll use them constantly.
Set unordered collection automatically rejects duplicates. Ideal for gathering unique IDs before running queries.
Map key-value pairs where keys are unique. The most powerful collection type and the backbone of trigger bulkification patterns.
Shallow Copy vs Deep Copy - it is the difference between copying a reference and copying actual data. Use .clone() and .deepClone() to keep your copies independent.
These are the actual tools you use every day when building on the Salesforce platform. Triggers, batch jobs, integrations, Lightning components - all these rely on these fundamentals.
If you want to go deeper with structured training and gain hands-on experience on real projects along with certification, check out Salesforce Development training at MindMajix. It covers Apex, SOQL, Lightning, Visualforce, and everything else you need to become a production-ready developer.
No. Java experience is good to have because the syntax is similar, but it is not a mandatory requirement.Many people pick up Apex as their first real programming language. What is more important is understanding Salesforce-specific patterns like governor limits, SOQL, and bulkification.
If you are consistently studying, you can learn the fundamentals in 4 to 6 weeks. Getting comfortable enough to write triggers and batch jobs for a real project usually takes about 3 months of complete practice.
Start with List; it's the most commonly used and every SOQL query returns one. Then learn Set for deduplication. Finally, learn Map. It's the most powerful and the most important for writing efficient trigger code.
People assign one sObject variable to another using = and assume they have two separate copies, but they don't. They have two references to the same object. Then they modify one and can't figure out why the other changed. Use .clone() when you need independence.
The Developer Console in any Salesforce org works great for testing. You can sign up for a free Salesforce Developer Edition and start writing code right away. For structured learning with projects and mentorship, take a look at MindMajix's Salesforce training.
Yes, MindMajix has several free resources:

Our work-support plans provide precise options as per your project tasks. Whether you are a newbie or an experienced professional seeking assistance in completing project tasks, we are here with the following plans to meet your custom needs:
| Name | Dates | |
|---|---|---|
| Salesforce Training | May 30 to Jun 14 | View Details |
| Salesforce Training | Jun 02 to Jun 17 | View Details |
| Salesforce Training | Jun 06 to Jun 21 | View Details |
| Salesforce Training | Jun 09 to Jun 24 | View Details |