Understanding Algorithm Complexity in Apex: A Developer’s Guide to Writing Performant Code

Why your Apex code works in sandbox but fails in production—and how to fix it

You’ve just deployed your trigger to production. It worked perfectly in your sandbox with 50 test records. But now, with 50,000 real-world records, your users are reporting timeouts, and your debug logs are filled with CPU time limit exceptions. Sound familiar?

Welcome to the world of algorithm complexity—where the difference between O(n) and O(n²) isn’t just academic theory, but the difference between code that scales and code that breaks.

In this guide, we’ll explore algorithm complexity specifically for Apex developers, with real-world examples, common pitfalls, and practical solutions you can apply today.

What Is Algorithm Complexity? (And Why Should You Care?)

Algorithm complexity, expressed using “Big O notation,” measures how your code’s performance scales as your data grows. It’s not about how fast your code runs right now—it’s about what happens when you go from 10 records to 10,000.

Think of it like this:

  • O(1) is like looking up a page number in an index—instant, regardless of book size
  • O(n) is like reading every page once—takes longer with more pages, but predictably
  • O(n²) is like comparing every page to every other page—manageable for short stories, impossible for encyclopedias

In Apex, where you’re bound by governor limits (CPU time, heap size, SOQL queries), understanding complexity isn’t optional—it’s survival.

The Complexity Hierarchy: From Lightning Fast to Dangerously Slow

Let’s break down the most common complexities you’ll encounter in Apex, from best to worst:

O(1) – Constant Time: Your Best Friend

Operations that take the same time whether you have 10 records or 10,000.

// Set and Map lookups are O(1) - always instant
Set<Id> accountIds = new Set<Id>{acc1.Id, acc2.Id, acc3.Id};
Boolean exists = accountIds.contains(targetId); // O(1)
Map<Id, Account> accountMap = new Map<Id, Account>([SELECT Id, Name FROM Account]);
Account acc = accountMap.get(someId); // O(1)
// Direct index access is also O(1)
List<String> names = new List<String>{'Alice', 'Bob', 'Charlie'};
String first = names[0]; // O(1)

Key Takeaway: Sets and Maps are your go-to data structures for lookups. Always prefer Set.contains() over List.contains().

O(n) – Linear Time: Efficient and Scalable

Time grows proportionally with data size. This is usually acceptable and often unavoidable.

// Single pass through data
List<Account> accounts = [SELECT Id, Name, AnnualRevenue FROM Account];
// Calculating total revenue - must visit each record once
Decimal totalRevenue = 0;
for (Account acc : accounts) { // O(n)
totalRevenue += acc.AnnualRevenue;
}
// Filtering data - also O(n)
List<Account> highValueAccounts = new List<Account>();
for (Account acc : accounts) {
if (acc.AnnualRevenue > 1000000) {
highValueAccounts.add(acc);
}
}

O(n²) – Quadratic Time: The Silent Killer

This is where things get dangerous. Performance degrades exponentially as data grows.

The Numbers Don’t Lie (Taken as Reference)

  • 100 records: 10,000 operations (fine)
  • 500 records: 250,000 operations (starting to hurt)
  • 1,000 records: 1,000,000 operations (CPU timeout likely)

Here’s the most common way developers accidentally write O(n²) code:

// ❌ ANTI-PATTERN: List.contains() in a loop = O(n²)
public static List<String> removeDuplicates(List<String> items) {
List<String> uniqueItems = new List<String>();
for (String item : items) { // O(n)
if (!uniqueItems.contains(item)) { // O(n) - scans entire list!
uniqueItems.add(item);
}
}
return uniqueItems;
}
// Total: O(n) × O(n) = O(n²)

Why this breaks: List.contains() scans the entire list from start to finish. When you call it inside a loop, you’re scanning the list once for every element—creating a nested loop effect.

Real-World Apex Pitfalls (And How to Fix Them)

Pitfall #1: The “Remove Duplicates” Trap

The Problem:

public static List<String> removeDuplicates(List<String> items) {
List<String> result = new List<String>();
for (String item : items) {
if (!result.contains(item)) { // O(n) operation
result.add(item);
}
}
return result; // O(n²) total
}

The Solution: Use a Set for O(1) Lookups

public static List<String> removeDuplicates(List<String> items) {
Set<String> seen = new Set<String>();
List<String> result = new List<String>();
for (String item : items) {
if (!seen.contains(item)) { // O(1) operation!
seen.add(item);
result.add(item);
}
}
return result; // O(n) total
}

Performance Impact:

  • 1,000 items: ~1,000 operations vs ~1,000,000 operations
  • 1000x faster!

Pitfall #2: Nested Loops for Matching Data

The Problem:

// Matching Contacts to Accounts
for (Account acc : accounts) { // O(n)
Integer contactCount = 0;
for (Contact con : contacts) { // O(m)
if (con.AccountId == acc.Id) {
contactCount++;
}
}
acc.NumberOfEmployees = contactCount;
}
// Total: O(n × m) - quadratic when n ≈ m

For 200 Accounts and 1,000 Contacts, that’s 200,000 comparisons!

The Solution: Build a Map First

// Step 1: Group contacts by AccountId - O(m)
Map<Id, List<Contact>> contactsByAccount = new Map<Id, List<Contact>>();
for (Contact con : contacts) {
if (!contactsByAccount.containsKey(con.AccountId)) {
contactsByAccount.put(con.AccountId, new List<Contact>());
}
contactsByAccount.get(con.AccountId).add(con);
}
// Step 2: Use the map for instant lookups - O(n)
for (Account acc : accounts) {
List<Contact> relatedContacts = contactsByAccount.get(acc.Id);
acc.NumberOfEmployees = relatedContacts != null ? relatedContacts.size() : 0;
}
// Total: O(n + m) - linear!

Pitfall #3: SOQL in Loops (The Governor Limit Destroyer)

The Problem:

for (Account acc : accounts) {
// Query inside loop - hits 100 SOQL limit at 100 records!
List<Contact> contacts = [SELECT Id, Name
FROM Contact
WHERE AccountId = :acc.Id];
// Process contacts...
}

The Solution: Query Once, Process Many Times

// Collect all IDs first
Set<Id> accountIds = new Set<Id>();
for (Account acc : accounts) {
accountIds.add(acc.Id);
}
// Single query with all IDs
List<Contact> allContacts = [SELECT Id, Name, AccountId
FROM Contact
WHERE AccountId IN :accountIds];
// Build a map for fast access
Map<Id, List<Contact>> contactsByAccount = new Map<Id, List<Contact>>();
for (Contact con : allContacts) {
if (!contactsByAccount.containsKey(con.AccountId)) {
contactsByAccount.put(con.AccountId, new List<Contact>());
}
contactsByAccount.get(con.AccountId).add(con);
}
// Now process efficiently
for (Account acc : accounts) {
List<Contact> contacts = contactsByAccount.get(acc.Id);
// Process contacts...
}

Why This Matters: Governor limits allow only 100 SOQL queries per transaction. One query in a loop = disaster at scale.

Checklist: Is Your Code Scalable?

Before deploying to production, ask yourself:

  • ☑️ No SOQL in loops? Every query should be before the loop
  • ☑️ Using Sets/Maps for lookups? Never List.contains() in a loop
  • ☑️ Avoiding nested loops? Can you use a Map instead?
  • ☑️ Tested with 200+ records? Bulk scenarios reveal complexity issues
  • ☑️ Checked debug logs? CPU time and heap size under limits?
  • ☑️ Querying with indexed fields? WHERE clauses on indexed fields perform better

The Bottom Line

Algorithm complexity isn’t just computer science theory—it’s the difference between Apex code that scales gracefully and code that crashes in production.

Three key principles:

  1. Sets and Maps are your friends – Use them for all lookups (O(1))
  2. Avoid nested loops – Usually solvable with a Map (O(n) vs O(n²))
  3. Query once, use many – Never SOQL in loops

The best part? Once you internalize these patterns, writing performant Apex becomes second nature. You’ll instinctively reach for a Map instead of a nested loop, and you’ll catch O(n²) anti-patterns in code review before they reach production.

Remember: Code that works with 50 records in sandbox isn’t necessarily code that works with 50,000 records in production. Think in terms of complexity, test at scale, and your users will thank you.

Additional Resources

What complexity challenges have you faced in Apex? Share your experiences in the comments below!

Leave a comment