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 instantSet<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 dataList<Account> accounts = [SELECT Id, Name, AnnualRevenue FROM Account];// Calculating total revenue - must visit each record onceDecimal 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 Accountsfor (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 firstSet<Id> accountIds = new Set<Id>();for (Account acc : accounts) { accountIds.add(acc.Id);}// Single query with all IDsList<Contact> allContacts = [SELECT Id, Name, AccountId FROM Contact WHERE AccountId IN :accountIds];// Build a map for fast accessMap<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 efficientlyfor (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:
- Sets and Maps are your friends – Use them for all lookups (O(1))
- Avoid nested loops – Usually solvable with a Map (O(n) vs O(n²))
- 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
- Salesforce Developer Limits: Execution Governors and Limits
- Apex Best Practices: Trigger and Bulk Request Best Practices
- Testing at Scale: Always test with 200 records minimum in triggers
What complexity challenges have you faced in Apex? Share your experiences in the comments below!