Data structures: choosing by the operations you need
Wednesday, 07:40: the nightly price import, 55 seconds in January, ran for 41 minutes and only finished at 07:40, after guests woke up. Search showed yesterday's prices, and nobody had touched the code.
Lists, maps, sets, trees and queues each make some jobs quick and others slow. Pick by what you'll do most, and one line of code can be 1,000 times faster.
- Problem
A hotel search endpoint that was instant with 500 hotels takes 4 seconds with 200,000.
- Why the simple system fails
Storing everything in one array and scanning it for every lookup costs time proportional to the data. It's invisible at small sizes and crushing at large ones.
- Engineering concept
Each data structure makes some operations cheap and others expensive. Pick the structure for the operations you do most: lookup by key, range, order, first-in-first-out, relationships.
- Technology
Arrays, hash maps and sets, balanced trees and B-trees, queues, stacks and heaps, graphs; their production forms: indexes, Redis, message queues, graph traversals.
- Trade-offs
Faster reads cost memory, slower writes or lost ordering. There's no best structure, only the best fit for the access pattern.
The problem
2 min- Omar · On-call SREPrice import: 55 s in January, 41 min last night. Search showed yesterday's prices until it finished.
- You · Engineer at StayHubWe went from 180 hotels to 1,200: about 7× more data. Why 45× slower?
- Lina · Senior engineer, your mentorGuess before we open the code.
The big idea
4 minPick the data structure for the operation you do most.
The code isn't slow; the shape is wrong. We look rows up by key 240,000 times, and we stored them in a structure made for reading in order.
How it works
5 minA data structure is a way of arranging data so certain operations are fast and others slow. The import keeps 240,000 rows (1,200 hotels × 200 nights) in an : items in order, instant by position, but finding by value means checking items one by one.
- 1Take one update
"Hotel h42, 3 Nov, €118." There are 240,000 of these.
- 2Scan for its row
rows.find(...)checks row 1, row 2, row 3… until hotel and date match. On average, half the list: about 120,000 checks. - 3Repeat for every update
240,000 × 120,000 ≈ 29 billion comparisons.
- 4Grow the data
7× more updates, each scanning a 7× longer list: 7 × 7 ≈ 45× the work.
- 5Use the right shape
A (key → value) reaches any key in about the same time, however many there are. No order. Keyed by
hotelId:date: about 480,000 operations in total.
Each structure makes some operations cheap: hash map / for get by key or "is it here?"; sorted structures like a index for ranges and "cheapest first"; a for first in, first out. Ask: what will I do with this data most often?
Watch a decision being made
4 minLina fixes the 41-minute import
The import matches 240,000 price updates against 240,000 rows and takes 41 minutes. Make it fast without new hardware, and prove it.
- 1List the operations
Find a row by hotel + date (240,000 times), change its price (240,000 times), save all rows (once).
Why: The structure follows from the most frequent operation: "get by exact key".
- 2Pick the structure and its key
Exact-key lookup → a hash map. The key is what identifies a row:
"h42:2026-11-03".Why: The key must be exactly what the update knows, and unique per row.
- 3Build once, look up many times
Fill the map once, outside the loop.
tsconst byKey = new Map<string, Row>(); for (const r of rows) byKey.set(`${r.hotelId}:${r.date}`, r); // 240,000 inserts, once for (const u of updates) { const row = byKey.get(`${u.hotelId}:${u.date}`); // one direct lookup if (row) row.priceCents = u.priceCents; else unknown.push(u); // count, don't crash }Why: Building the map inside the loop would cost as much as the scan.
- 4Check the cost, measure again
About 40 MB of memory on an 8 GB server. On last night's data: 0.4 s instead of 41 min.
Why: Faster lookups always cost something, usually memory. A fix you haven't measured is a hope.
In real life
3 minTwitter (X): a ready-made list per user
- Has publicly described keeping each home timeline as a precomputed list in Redis
- Reading is just reading the top of a list
- Cost: every post is copied into many lists
Worth it when reads vastly outnumber writes, at huge scale.
A typical web app: an index, queried on demand
- Each item stored once, with a B-tree index on (owner, created_at)
- "Latest 50" is one query reading 50 rows of the sorted index
Worth it for most apps until the scale is huge.
What goes wrong
3 min10,000 × 10,000 = 100 million comparisons, growing with the square of the data.
Build a Map or Set once before the loop.
"Cheapest 20" or "between dates" means scanning and sorting everything.
A sorted structure: a B-tree index.
For 50 items it's microseconds, and simpler code wins.
Optimise large data or hot loops, after measuring.
Trade-offs
3 min| Structure | Fast at | You pay | Can't do |
|---|---|---|---|
| Array | By position; reading in order | Scans to find by value | Fast lookup unless sorted |
| Hash map / set | Get by key, "is it here?" | More memory per item | Ranges, order |
| B-tree | Ranges, sorted reads | Slower writes per index | Beat a map at exact lookups |
- Karim · CTOLoad all hotels, prices and rooms left into a Map at startup. No database trips. Reload it every minute.
- Lina · Senior engineer, your mentorHotel names and photos, yes. But rooms left changes every few minutes.
- Karim · CTOMemory is cheap.
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.
- What the code mostly does with the data (by key, by range, "is it here?", in order), because that picks the structure.
- How big the data will get, not how big it is today: 180 to 1,200 hotels turned 55 s into 41 minutes.
- What counts as "the same" item: two guest objects with one email are one guest, so de-duplicate on the email.
Our nightly price import takes 41 minutes (55 s in January). It applies 240,000 updates to 240,000 price rows, searching the whole list with rows.find for each. Build a Map keyed by "hotelId:date" once, then look each update up. Keep the output identical. First tell me the operation count before and after and the Map's memory. Then run it on last night's file on staging, compare with the old output row by row, and report the run time.
- Claude explains the counts: about 29 billion comparisons before, about 480,000 operations after.
- The new import produces exactly the same 240,000 prices, not just "runs faster".
- The key "hotelId:date" identifies one row; no two rows share a key.
- A bigger server or parallel batches, with
findstill inside the loop. - A speed-up measured on a 100-row test file.
- A hash map used for "prices between €80 and €120".
Check yourself
2 minfor (const g of incoming) {
const existing = guests.find(x => x.email === g.email);
if (!existing) guests.push(g);
}Next episode: a hotel chain with 10,800 hotels wants to join, taking us from 1,200 to 12,000 hotels. Karim asks what breaks, before the data even exists.
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
| Structure | Lookup by key | Insert | Ordered / range | Good for |
|---|---|---|---|---|
| Array | O(n) unsorted · O(log n) sorted (binary search) | O(1) at end · O(n) at front | Only if kept sorted | Ordered lists, small n |
| Hash map / set | O(1) average | O(1) average | No | Lookup by id, de-duplication, counting |
| Balanced tree / B-tree | O(log n) | O(log n) | Yes | Ranges, sorted reads, DB indexes |
| Queue (FIFO) | n/a | O(1) | Arrival order | Jobs, buffering, fairness |
| Stack (LIFO) | n/a | O(1) | Reverse arrival | Undo, depth-first search |
| Heap (priority queue) | O(1) peek min | O(log n) | Only the min/max | Schedulers, top-k |
| Graph | Depends on representation | O(1) add edge | No | Relationships, routes |
Lina fixes the 41-minute import
The nightly import matches 240,000 price updates against 240,000 rows and takes 41 minutes. Make it fast without new hardware, and prove it.
- 1List the operations
Write what the import does and how often: find a row by hotel + date (240,000 times), change its price (240,000 times), save all rows (once).
Why: The structure follows from the most frequent operation. Here it's "get by exact key", by far.
- 2Count the work
findchecks about half the list per call: 240,000 × 120,000 ≈ 29 billion comparisons. In January: 36,000 × 18,000 ≈ 650 million.Why: Counting explains the 45× jump and tells you the fix must remove the scan, not speed it up.
- 3Pick the structure for that operation
Exact-key lookup → a hash map. The key is what identifies a row:
"h42:2026-11-03"(hotel and date together).Why: A map reaches a key in about the same time whatever its size. The key must be exactly what the update knows.
- 4Build once, look up many times
Fill the map once, outside the loop, then use it inside.
tsconst byKey = new Map<string, Row>(); for (const r of rows) byKey.set(`${r.hotelId}:${r.date}`, r); // 240,000 inserts, once for (const u of updates) { const row = byKey.get(`${u.hotelId}:${u.date}`); // one direct lookup if (row) row.priceCents = u.priceCents; else unknown.push(u); // count, don't crash }Why: Building the map inside the loop would cost as much as the scan. Once outside: about 480,000 operations in total.
- 5Check the cost
The map holds 240,000 small entries: about 40 MB on a server with 8 GB. Writing to it is fast too.
Why: Faster lookups always cost something, usually memory. Check that the bill is affordable before shipping.
- 6Measure again
Run on last night's data: 0.4 s instead of 41 min. Add the
unknowncount to the import log.Why: A fix you haven't measured is a hope. The
unknowncount catches updates for hotels we don't have.
-- The same idea inside PostgreSQL: an index is a sorted structure (a B-tree)
CREATE INDEX idx_rooms_city_price ON rooms (city, price);
SELECT * FROM rooms WHERE city = 'Lisbon' AND price < 12000 ORDER BY price LIMIT 20;Interactive simulation
Visual diagram
One hotel platform, five access patterns, five structures (in memory and in infrastructure).
Analogy
- At a hotelThe guest register, line by lineIn codeAn array (list)
- At a hotelThe key rack: slot 314 for room 314In codeA hash map
- At a hotelThe VIP list: on it or not?In codeA set
- At a hotelReservations book sorted by dateIn codeA sorted tree
- At a hotelThe line at check-inIn codeA queue
- At a hotelThe concierge's city mapIn codeA graph
Where the analogy breaks: a key rack has a fixed number of slots. A hash map grows as you add keys, and it quietly uses extra memory to stay fast.
Check your understanding
Practice ladder
Trace the blocked-cards check
Scenario Warm-upAt payment, StayHub checks whether a card is on the fraud list:
const blocked: string[] = loadBlockedFingerprints(); // 50,000 fingerprints
function isBlocked(fp: string) { return blocked.includes(fp); }StayHub takes about 400 payments a day. For one payment with a card that is not blocked, how many comparisons does isBlocked do? And for one day? Then say what changes if blocked becomes a Set.
Finish Lina's grouping code
Code PracticeEach morning, StayHub emails every hotel owner the list of yesterday's bookings. There are 1,200 hotels and about 400 bookings a day. The current code:
for (const hotel of hotels) {
const theirs = bookings.filter(b => b.hotelId === hotel.id);
if (theirs.length) sendDigest(hotel, theirs);
}Lina's rewrite has gaps. Fill in the two ___ lines, and say how many comparisons the old and new versions do.
const byHotel = new Map<string, Booking[]>();
for (const b of bookings) {
___ // get this hotel's list, or a new empty one
___ // add the booking and store the list
}
for (const [hotelId, theirs] of byHotel) sendDigest(hotelsById.get(hotelId)!, theirs);"Recently viewed hotels"
Design PracticeMaya: "On the home page, show each guest the last 5 different hotels they viewed, newest first."
Guests view about 20 hotels per visit, often the same hotel several times. Choose the data structure(s) for one guest's list, and explain what happens on each view and on each home-page load. Say what you'd do about duplicates and old entries.
Index every column?
Decision StretchThe bookings table has 14 columns and gets about 400 inserts a day, plus updates when guests change dates. The slow queries Omar found:
- find bookings by
guest_email(guest support, 300 times a day) - latest bookings for one hotel by
(hotel_id, created_at)(owner dashboard, 8,000 times a day)
A teammate proposes indexing all 14 columns "to be safe". Write your decision: "We chose X over Y because…, accepting the risk of…".
Explain it yourself
Explain how you'd choose between an array, a hash map and a tree for a new feature, using a hotel example.