Big O intuition: how code slows down as data grows
Monday, 10:05: a hotel chain wants to bring its 10,800 hotels to StayHub, in waves from next month: ten times the 1,200 we have. Karim needs to know by Friday what breaks, and the data to test with doesn't exist yet.
No maths anxiety needed. Big O is just a way to ask: when the data grows 10 times, how much more work does my code do?
- Problem
Code that answers in 5 ms on 1,000 test records takes 3 hours on 1,000,000 production records.
- Why the simple system fails
Benchmarks on small data hide how work grows. A nested loop is invisible at n = 100 and catastrophic at n = 100,000.
- Engineering concept
Big O describes how the number of steps grows with input size n, ignoring constants: O(1), O(log n), O(n), O(n log n), O(n²).
- Technology
Hash maps for O(1) lookups, sorting plus binary search and database B-tree indexes for O(log n), Sets instead of nested loops.
- Trade-offs
Faster lookups cost memory and slower writes (indexes, sorted data). Constants still matter for small n, so measure before optimising.
The problem
2 min- Karim · CTOA chain wants to bring its 10,800 hotels, in waves from next month. We have 1,200. What breaks? Answer by Friday.
- You · Engineer at StayHubWe can't load-test with hotels we don't have yet.
- Lina · Senior engineer, your mentorWe can reason. The nightly duplicate check compares every hotel with every other. Guess how it grows.
The big idea
4 minBig O tells you how work grows when the data grows, not how many milliseconds it takes.
So you can answer Karim on Monday, without test data: for each piece of code, ask "10× the data, how much more work?"
How it works
5 minn is the size of the input: hotels, bookings, rows. describes the shape of the growth as n grows. It ignores whether a step takes 1 µs or 5 µs.
- 1O(1), constant
Same work at any size. A lookup by id: 1,200 or 12,000 hotels, one step.
- 2O(log n)
Halve what's left each step: , a database . 1,200 → about 10 steps; 12,000 → about 14; 1,000,000 → about 20.
- 3O(n), linear
Touch every item once: a scan, a sum, a . 10× data, 10× work.
- 4O(n log n)
The cost of a good sort. A bit more than linear; fine for millions.
- 5O(n²), quadratic
Every pair: nested loops, or a
findinside a loop. 10× data, 100× work.
The duplicate check: 1,200 × 1,200 ÷ 2 = 720,000 pairs at about 55 µs each: 40 s. At 12,000: 72 million pairs, about 67 minutes. The ÷ 2 and the 55 µs set the exact time; the shape decides whether 10× data means 1×, 10× or 100× the work.
Watch a decision being made
4 minAuditing StayHub for 10× the hotels
From 1,200 to 12,000 hotels, without test data. Find which code paths break and fix the worst one.
- 1List the paths, find n and the shape
Hotel page by id (Map): O(1). Search (indexed): O(log n). Nearby-hotels widget, scanning all hotels per view: O(n). Nightly duplicate check: O(n²).
Why: The shape, not today's speed, tells you what happens at 10×.
- 2Multiply by the growth
Page 1 → 1 step; search ~10 → ~14; nearby 4 → 40 ms per view; duplicates 40 s → about 67 min.
Why: "It's O(n²)" means nothing to the business; "67 minutes every night" does.
- 3Fix the steepest curve
Hotels at different postcodes can't be duplicates. Group by postcode in one pass, compare pairs only inside each group.
tsconst byPostcode = new Map<string, Hotel[]>(); for (const h of hotels) { // O(n): one pass const list = byPostcode.get(h.postcode) ?? []; list.push(h); byPostcode.set(h.postcode, list); } for (const group of byPostcode.values()) comparePairs(group); // small groupsWhy: You need fewer comparisons, not faster ones. Groups of ~20 drop 72 million pairs to under a million.
- 4Watch, don't fix, the linear one
Nearby at 40 ms per view is acceptable. Note it, with the n where it will hurt.
Why: Fix what breaks; write down what breaks next.
In real life
3 minripgrep / grep: scan every time
- Reads every file on each search: O(n)
- No index to build; always sees the latest files
- Fine for one project, too slow for all of GitHub
Right when n is small enough to scan in a moment.
GitHub code search: build an index
- Has publicly described building its own search index for code
- Each search looks up the index instead of reading every file
- Cost: the index is rebuilt as code changes
Right when n is enormous and searches are constant.
What goes wrong
3 minAt n = 100, O(n²) and O(n) look the same.
Test with production-sized data, or reason about 10× and 100×.
Each call is O(n), so the loop is O(n²) with no visible nested for.
Build a Set or Map once before the loop.
Big O hides constants: scanning 8 currency codes can beat hashing.
For small, capped n, keep the simplest code.
Trade-offs
3 min| Option | Fits when | You pay | Main risk |
|---|---|---|---|
| Scan, O(n) | Small or capped n, rare runs | Nothing up front | Grows with the data |
| Index, O(log n) | Frequent lookups on large, growing data | Slower writes, storage | Helps only the indexed key |
| Hash map, O(1) | Many exact-key lookups in memory | Memory for the whole map | No ranges; must stay in sync |
- Karim · CTOMove the duplicate check to a server with 4× the cores. Today, no code changes.
- Lina · Senior engineer, your mentorThat's about 17 minutes. The next chain puts us back. Grouping by postcode takes a day and fixes the shape.
- Karim · CTOA day not spent on the chain's onboarding. And grouping might miss duplicates with a postcode typo.
Your job as tech lead
4 minClaude Code can write the code for this. You still own the decisions, and you have to know what good looks like to accept or reject its work.
- The size you design for: 12,000 hotels after the chain deal, not today's 1,200.
- Which paths are hot (every hotel-page view) and deserve attention, and which are small and bounded (8 currency codes) and stay simple.
- Whether the database does the filtering, with an index, instead of loading 1.2 million bookings into the app.
A chain is taking StayHub from 1,200 to 12,000 hotels. Before we import them, audit four paths and give each its Big O and estimated time today and at 12,000: hotel page by id, search by city and price, the "Nearby hotels" widget, and the nightly duplicate check comparing every pair. Don't change code yet. For anything worse than 10× slower at 10× data, propose a fix and its cost (memory, an index, precision). Show the arithmetic, not just labels.
- The pairwise check is 720,000 pairs today and 72M at 12,000: 100× the time, not 10×.
- Claude points at the exact line that makes a path O(n) or O(n²), like a
findinside a loop. - Timings use 12,000 generated hotels on staging, not the 50 in test fixtures.
- It optimises a path that's already O(1) and leaves the O(n²) job alone.
- A bigger server for work that grows 100× when data grows 10×.
- It replaces
mapwith aforloop and calls it a complexity fix.
Check yourself
2 minNext episode: the chain's first 300 hotels are in, and their hotel pages wait on three services one after another. Guests stare at a spinner for 2.4 seconds.
Mark it complete to update your skill map.
Go deeper (optional)You don't need this to understand the lesson. Open it when a project needs the details: Technical deep dive, Interactive simulation, Visual diagram, Analogy, Check your understanding, Practice ladder, Explain it yourself.
Technical deep dive
Lina's audit method fits on a sticky note: list the paths, find n, find the shape, multiply, fix the steepest. This time you estimate first and Lina checks.
Auditing StayHub for 10× the hotels
The chain would bring StayHub from 1,200 to 12,000 hotels, in waves from next month. Without test data, find which code paths break and fix the worst one.
- 1List the paths that touch hotels
Hotel page by id (a
Map), search by city and price (indexed query), the nearby-hotels widget (scans all hotels per page view), the nightly duplicate check (every pair).Why: You can only estimate code you've named. Paths that run per request matter more than paths that run once a night.
- 2Find n and the shape of each
Map lookup: O(1). Indexed search: O(log n). Nearby: O(n) per view. Duplicates: O(n²) per night.
Why: The shape, not today's speed, tells you what happens at 10×. A path that's fast today can be the steepest curve.
- 3Multiply by the growth
At 10× hotels: page 1 step → 1 step; search ~10 → ~14 steps; nearby 4 ms → 40 ms per view; duplicates 40 s → about 67 min.
Why: Turning shapes into numbers lets Karim decide. "It's O(n²)" means nothing to the business; "67 minutes every night, competing with the other nightly jobs for the database" does.
- 4Fix the steepest curve
Two hotels at different postcodes can't be duplicates. Group by postcode in one pass, then compare pairs only inside each group.
tsconst byPostcode = new Map<string, Hotel[]>(); for (const h of hotels) { // O(n): one pass const list = byPostcode.get(h.postcode) ?? []; list.push(h); byPostcode.set(h.postcode, list); } for (const group of byPostcode.values()) comparePairs(group); // small groupsWhy: You don't need a faster comparison; you need fewer comparisons. With groups of ~20 hotels, pairs drop from 72 million to well under a million.
- 5Decide what to watch, not fix
Nearby at 40 ms per view is acceptable today. Note it, and plan a geo index if hotels grow 10× again.
Why: Not every linear path needs work now. Fix what breaks; write down what will break next, with the n where it will.
// Hidden O(n × m): includes is a scan, inside a loop.
bookings.map((b) => ({ ...b, vip: vipEmails.includes(b.email) }));
// O(n + m): build a Set once, then each check is one step.
const vips = new Set(vipEmails);
bookings.map((b) => ({ ...b, vip: vips.has(b.email) }));| Shape | n = 1,000 | n = 1,000,000 | Typical example |
|---|---|---|---|
| O(1) | 1 step | 1 step | Map get, array[i] |
| O(log n) | ~10 | ~20 | Binary search, B-tree index |
| O(n) | 1,000 (~10 µs) | 1M (~10 ms) | Scan, sum, full table scan |
| O(n log n) | ~10k (~0.1 ms) | ~20M (~0.2 s) | Sorting |
| O(n²) | 1M (~10 ms) | 10¹² (~3 hours) | Nested loops, every pair |
Interactive simulation
Visual diagram
Three ways to answer "find booking for guest@example.com" among 1,000,000 bookings (roughly StayHub's size today: ~1.2M and growing). Start the walkthrough.
Analogy
- Finding a guest at a hotelGrabbing hook 412 from the key rackGrowth shapeFlat: O(1)
- Finding a guest at a hotelFlipping an alphabetical register in halvesGrowth shapeHalving: O(log n)
- Finding a guest at a hotelReading a messy pile of cardsGrowth shapeStraight line: O(n)
- Finding a guest at a hotelSorting the pile into a register firstGrowth shapeSorting: O(n log n)
- Finding a guest at a hotelComparing every card with every otherGrowth shapeExplodes: O(n²)
Where the analogy breaks: a person gets tired, a computer doesn't. But a computer meets millions of cards, where the shape of growth matters far more than how fast each single step is.
Check your understanding
Practice ladder
Trace the growth of three functions
Estimation Warm-upThree functions run on StayHub's 1.2 million bookings:
// A const total = bookings.reduce((s, b) => s + b.priceCents, 0); // B const guest = bookingsByEmail.get(email); // a Map // C const repeat = bookings.filter((b) => bookings.some((o) => o.guestEmail === b.guestEmail && o.id !== b.id));
For each, give its shape, and say how its work changes when bookings grow from 1.2 million to 2.4 million.
Finish Lina's estimate table
Estimation PracticeLina's audit table for the chain (1,200 → 12,000 hotels) has two empty cells. Fill them in, with a one-line reason for each.
| Path | Shape | Today | At 12,000 hotels |
|---|---|---|---|
| Hotel page (Map by id) | O(1) | 1 ms | 1 ms |
| Sitemap generation (sort hotels by name once a day) | O(n log n) | 0.5 s | ___ |
Photo check (for each hotel, look up its photos in an array of all photos with filter) | ___ | 20 s | 2,000 s (~33 min) |
The owner report that times out
Debugging PracticeThe monthly owner report shows, for each hotel, its bookings and its reviews. It worked for 1,200 hotels; on staging with the chain's 12,000 it times out after 30 s.
for (const hotel of hotels) { // 12,000
const b = await db.query("SELECT * FROM bookings WHERE hotel_id = $1", [hotel.id]);
const r = reviews.filter((x) => x.hotelId === hotel.id); // 300,000 reviews in memory
report.push({ hotel, bookings: b.rows, reviews: r });
}bookings.hotel_id has no index. Find every growth problem in this loop and fix them.
Optimise the currency list?
Decision StretchIn code review, a teammate flags this on the checkout page:
const SUPPORTED = ["EUR", "GBP", "USD", "CHF", "JPY", "SEK", "NOK", "DKK"];
if (!SUPPORTED.includes(req.currency)) return badRequest("unsupported currency");"includes is O(n)! Use a Set." Karim asks you to decide. Write it as "We chose X over Y because…, accepting the risk of…", and say when you'd change your mind.
Explain it yourself
Explain to a junior teammate why finding a booking by email among 1,000,000 rows can take 20 steps or 1,000,000 steps, and what decides which one you get.