Be Software Architecture Wise
Be Wise Apps

Leer en español · Lire en français · Auf Deutsch lesen

Section 0.2 of 0.8 · Beginner · 16 min read

Data, data structures and algorithms, and why speed depends on them

Video lesson

5 min · narrated, with captions
Listen only (audio)
Read the transcript

Imagine looking for one letter in a post office. If the letters are in a heap, you check them one by one, and a bigger heap takes longer. If they are sorted into pigeonholes by postcode, you walk straight to the right hole, however many letters there are. That difference is what this lesson is about.

First, what is data made of? Computers store everything as bits: binary digits that are either zero or one. Eight bits make a byte. On their own, bits mean nothing. The same pattern could be the number sixty five or the letter A. A data type is the agreement about how to read them. Integers are whole numbers, for counting. Booleans are true or false. Strings are text. Floating point numbers are decimals, but only approximately, so money should be stored as whole pence or cents, or with a decimal type.

A data structure is a way of arranging many values so that the operations you need are quick. Here are six common ones. A list keeps items in order, like a numbered shopping list. Getting the item at a position is instant, but finding a name means checking items one by one. A dictionary stores values under unique keys, like a coat check. Hand over ticket C three seven seven, and you get the right coat at once. A queue is first in, first out, like orders waiting for a kitchen. A stack is last in, first out, like a pile of plates, or your browser's Back button. A tree has one root with branches beneath it, like the folders on your computer. A graph links items in any pattern, like a road map.

An algorithm is a precise, step by step method for solving a problem that always finishes. Different algorithms for the same problem can do very different amounts of work. Here is binary search in a sorted list of sixteen customer IDs. We want zero three seven seven. Compare it with the middle item. It's bigger, so the whole first half is ruled out. Compare with the middle of what's left. Now it's smaller, so the upper part goes too. Each look halves what remains. Two more looks, and it's found. For a million sorted names, binary search needs about twenty looks instead of up to a million.

Engineers describe this with Big-O notation. It asks: if the data gets much larger, how much more work is there? Constant time means the work doesn't grow, like a dictionary lookup, on average. Logarithmic means one more step each time the data doubles, like binary search. Linear means double the data, double the work, like scanning a list. Quadratic means double the data, four times the work, which happens when every item is compared with every other item. Code that was fine with a thousand customers can grind to a halt with a million.

Now a worked example. A food delivery app stores a million customers, and each time an order arrives it must find the customer by their ID. Design A scans a list. On average that's about half a million checks, or roughly fifty milliseconds per lookup. At two hundred orders a second, that's ten seconds of processor work every second. The server cannot keep up, so requests queue and time out. Design B uses a dictionary keyed by ID. It goes straight to the entry: about one check, whether there are a thousand customers or a million. Design B isn't free. It uses extra memory and must be kept up to date when customers change. But it turns failure into comfortable headroom.

Here's the difference. At the top, scanning compares the wanted ID with each customer in turn. A million customers can need a million comparisons. Below, the dictionary runs the key through a hash function, which says C three seven seven lives in slot four, and goes straight there. One step, whatever the number of customers.

The dictionary is faster because it uses extra memory to organise the data in advance. That's one of the most common trade-offs in computing: buying speed with space. An index, like the one at the back of a book, speeds up reads, but it takes storage and must be updated on every write. A cache keeps a copy of an expensive answer somewhere fast. But when the data changes, how stale may the copy be? A stale price for a few seconds may be fine. A stale bank balance may not. So architects ask which questions must be answered quickly, and how large the data will grow. No amount of extra hardware will rescue a quadratic algorithm for long.

The full section has more on text, a table comparing the data structures, and the numbers behind Big-O. Next, in Section zero point three, we turn from data to code, and how it's organised. See you there.

What you need to know

Everything is bits

Computers store everything as bits: binary digits that are either 0 or 1. Physically a bit might be a tiny electrical charge, a magnetic region on a disk or a pulse of light in a fibre, but to software it is just one of two values. Eight bits make a byte, which can hold 28 = 256 different patterns, from 00000000 to 11111111.

Sizes build up from there. A kilobyte is about a thousand bytes, a megabyte about a million, a gigabyte about a billion and a terabyte about a trillion. (Strictly, the standard prefixes kilo, mega and giga mean powers of 1,000; memory is often measured in powers of 1,024, for which the units kibibyte, mebibyte and gibibyte exist. The difference rarely matters for design but explains why a "1 TB" disk shows less in some file managers.) A page of plain text is a few kilobytes, a phone photo a few megabytes, an hour of high-definition video a few gigabytes.

Bits mean nothing on their own. The pattern 01000001 could be the number 65, the letter A, part of a colour in a photo or part of an instruction for the CPU. What gives bits meaning is the agreement about how to read them, which is the idea of a data type.

Data types: numbers, text and true or false

A data type tells a program what kind of value a piece of data is, and therefore what operations make sense. Every programming language has a few basic types, and Python's are typical.

Integers are whole numbers such as 3, 0 or -42, used for counting things: items in a basket, seats on a flight. Floating-point numbers (floats) such as 12.5 or 0.2 represent decimals approximately, in binary. Because many decimals have no exact binary form, 0.1 + 0.2 in Python gives 0.30000000000000004. That is fine for physics and graphics but wrong for money, which is why financial systems store amounts as whole numbers of pence or cents, or use a decimal type (Python has decimal.Decimal). Booleans have just two values, True and False, and record yes-or-no facts such as "has this order been paid?". Strings are text, such as a customer's name. Real systems add more types on top: dates and times, money, and binary data such as images.

Types matter beyond a single program. When two systems exchange data, they must agree on types as well as on values: is a postcode a number or text (text: some begin with letters, and some numeric codes begin with zero)? Is a date in day-month or month-day order, and in which time zone? Many expensive integration bugs are type disagreements.

Text and UTF-8

Text is stored by giving every character a number and then storing the numbers as bytes. The international standard Unicode assigns a number, called a code point, to well over a hundred thousand characters from the world's writing systems, plus symbols and emoji. An encoding then decides how those numbers become bytes. Today the dominant encoding, on the web and in most new systems, is UTF-8.

UTF-8 uses a variable number of bytes per character: one byte for the basic English letters, digits and punctuation (so text that uses only these characters is byte-for-byte identical in UTF-8 to the much older ASCII encoding), two bytes for characters such as é, three for characters such as € and most Chinese, Japanese and Korean characters, and four for emoji and some rarer scripts. This has a practical consequence: the number of characters in a string and the number of bytes it occupies can differ. A database column limited to 50 bytes may not hold a 50-character name written in Greek.

The classic text bug is garbled characters (for example Café instead of Café), which appears when bytes written in one encoding are read as another. The cure is simple to state: use UTF-8 everywhere, and say so explicitly whenever text crosses a boundary, such as a file, a database or a network message.

Data structures: ways of arranging data

Programs rarely deal with one value at a time. A data structure is a way of organising many values in memory so that the operations you need are quick. The right choice can make the difference between a page that loads instantly and one that never loads. Each has an everyday analogy.

A list (called an array in many languages) keeps items in order, like a numbered shopping list. Getting the fifth item is instant, because the computer can calculate where it is. Finding an item by its value means checking items one by one. A dictionary (also called a map or hash table) stores values under unique keys, like a coat check: hand over ticket C377 and you get the right coat at once, without looking at every coat. A set is a collection with no duplicates, like a guest list, and answers "is this person on it?" quickly.

A queue works like a line at a post office counter: first in, first out. Systems use queues for work waiting to be done, such as orders waiting for a kitchen. A stack works like a pile of plates: last in, first out. Your browser's Back button uses one: the page you visited most recently is the first one you return to. A tree has a single root with branches beneath it, like a family tree or the folders on your computer; trees are also how databases keep data sorted for fast searching. A graph is items (nodes) joined by links (edges) in any pattern, like a road map or a social network, and answers questions such as "what is the shortest route?" or "who are friends of friends?".

Six common data structures

Six common data structures: list, dictionary, queue, stack, tree and graphList (array): items in order, found by positionAna0Ben1Chen2Dev3Eve4Dictionary (map): value found by key"C102"Ana"C215"Ben"C377"ChenQueue: first in, first outorder 4order 3order 2order 1joinservedStack: last in, first outpage 3 (top)page 2page 1push andpop at topTree: one root, each item has childrenFoodPizzaSushiCurryGraph: items linked in any patternABCDE
Each structure arranges the same kind of data differently, which makes different questions fast.

What each structure is good at

StructureFast atSlow atTypical use
List / arrayGetting the item at a position; adding to the endFinding an item by value (must scan); inserting near the frontAn ordered basket of items; a page of search results
Dictionary / mapFinding, adding or removing by keyAnything that needs the items in sorted orderCustomers by ID; settings by name; a cache
SetChecking membership; removing duplicatesKeeping order (most sets have none)Which users have already voted; unique tags
QueueAdding at the back and taking from the frontReaching items in the middleOrders waiting for the kitchen; jobs waiting for a worker
StackAdding and taking from the topReaching items lower downUndo history; the Back button; tracking function calls
Tree (sorted)Finding by value and listing a range in orderMore complex to keep balancedDatabase indexes; folders; menus
GraphFollowing relationshipsVery large graphs need specialised storageRoutes and maps; social networks; recommendations
basket = ["pizza", "cola", "pizza"]            # list: ordered, duplicates allowed
print(basket[0])                                # by position: "pizza"

customers = {"C102": "Ana", "C377": "Chen"}      # dictionary: key to value
print(customers["C377"])                        # by key: "Chen"

voted = {"ana@example.com", "ben@example.com"}   # set: no duplicates
print("ana@example.com" in voted)              # True

from collections import deque
kitchen = deque(["order 1", "order 2"])         # queue
kitchen.append("order 3")                       # joins at the back
print(kitchen.popleft())                        # served first: "order 1"
The same structures in Python

Algorithms

An algorithm is a precise, step-by-step method for solving a problem that always finishes: like a recipe, but with no room for interpretation. Sorting a list of names, finding the shortest route between two addresses, checking whether a password matches and deciding which advert to show are all done by algorithms.

There is usually more than one algorithm for the same problem, and they can differ enormously in how much work they do. Consider finding a name in a printed telephone directory. You could start at page one and read every entry until you find it: correct, but slow. Or you could open the book in the middle, see whether the name comes before or after that page, discard the half that cannot contain it and repeat. Because the directory is sorted, each look halves the remaining pages. This second method is called binary search, and for a directory of a million names it needs about 20 looks instead of up to a million.

Binary search: halving the problem

Binary search halves the part of a sorted list still to be searched at every stepBinary search in a sorted list of 16 customer IDs: looking for 0377Step 10102011801500167020102150240026303000311035003770402044404800499Step 20102011801500167020102150240026303000311035003770402044404800499Step 30102011801500167020102150240026303000311035003770402044404800499Step 40102011801500167020102150240026303000311035003770402044404800499foundHighlighted: the middle item compared at each step. Grey: ruled out. Items left: 16, 7, 3, 1
In a sorted list each comparison rules out half of what remains, so 16 items need at most 4 or 5 looks.

Big-O: how work grows with data

Engineers describe how an algorithm's cost grows using Big-O notation. It ignores the exact time on a particular machine and asks one question: if the amount of data, called n, gets ten or a thousand times larger, how much more work is there? It describes the worst case unless someone says otherwise, and it ignores constant factors, which is both its strength (it captures the shape of growth) and its weakness (two O(n) algorithms can still differ by a factor of ten in practice).

O(1), constant time: the work does not depend on n. Looking up a key in a dictionary is O(1) on average. O(log n), logarithmic: the work grows by one step each time n doubles, like binary search. O(n), linear: double the data, double the work, like scanning a list. O(n log n): slightly worse than linear; good general-purpose sorting algorithms work at this rate. O(n2), quadratic: double the data, four times the work. This typically appears when, for every item, a program loops over every other item, such as comparing each customer with every other customer to find duplicates.

How many basic steps, roughly, as data grows

n (items)O(1)O(log n)O(n)O(n log n)O(n2)
101about 310about 33100
1,0001about 101,000about 10,0001,000,000
1,000,0001about 201,000,000about 20,000,0001,000,000,000,000

Read the last column carefully. At a billion simple steps per second, a million-squared algorithm needs a trillion steps: around a quarter of an hour of pure computation, and in practice far longer. The same job done with an O(n log n) method takes a small fraction of a second. Code that looked fine in testing with 1,000 customers can grind to a halt with a million, and no amount of extra hardware will rescue a quadratic algorithm for long.

Worked example

Worked example: finding a customer among a million

A food-delivery app stores 1,000,000 customers. Each time an order arrives it must find the customer by ID. Compare two designs, assuming each check takes 0.1 microseconds.

Design A: a listfor c in customers: if c.id == wanted: ... scans until it finds a match.
Checks per lookup (A)Up to 1,000,000; on average about 500,000.
Time per lookup (A)About 500,000 x 0.1 microseconds = 50 milliseconds.
At 200 orders per second (A)200 x 50 ms = 10 seconds of CPU work every second: the server cannot keep up, so requests queue and time out.
Design B: a dictionary keyed by IDcustomers_by_id[wanted] computes where the entry is and goes straight there.
Checks per lookup (B)About 1, whether there are a thousand customers or a million.
Time per lookup (B)Well under a microsecond.
Cost of Design BExtra memory for the dictionary, and the work of building it and keeping it up to date whenever a customer is added or changed.

The two designs give identical answers; only the data structure differs, and that changes the outcome from failure to comfortable headroom.

Databases offer the same choice. Without an index, finding a row by a column means scanning the table; with an index it means a few steps through a sorted tree. The section on storing data returns to this.

Scan versus lookup

Finding a customer by scanning a list checks items one by one, while a dictionary jumps straight to the right slotScanning a list: check each customer in turnFindC377C102C215C311C350C3775 comparisons here; a million customers can need a millionDictionary lookup: compute where the key lives, go straight thereFindC377Hash functionC377 to slot 4slots 0 to 3 ...slot 4: Chen1 step, whateverthe number ofcustomers
A scan does work in proportion to the number of items; a dictionary goes directly to the right slot.

Space versus time

The dictionary in the worked example is faster because it uses extra memory to organise the data in advance. This is one of the most common trade-offs in computing: you can often buy speed with space, or save space by spending time.

An index is extra data, kept beside the main data, that answers a particular kind of question quickly, like the index at the back of a book. It speeds up reads, but it takes storage and must be updated on every write, so a table with ten indexes is slow to insert into. A cache keeps a copy of the answer to an expensive question somewhere fast, such as in memory, so the next person to ask gets it immediately. Caches are why a popular product page loads quickly even when the database behind it is busy, but they raise a hard question: when the underlying data changes, how quickly must the copy change too? Showing a stale price for a few seconds may be acceptable; showing a stale bank balance may not.

Compression is the opposite trade: spend CPU time to squeeze data into fewer bytes, which saves storage and network transfer time.

Why this matters for architecture

Architects rarely write sorting algorithms, but they constantly make decisions whose consequences are exactly these. Which questions will the system need to answer quickly, and does the data have the indexes to answer them? How large will the data grow in three years, and which parts of the design are linear or worse in that growth? Where is caching worth its complexity, and how stale may cached data be? When data volume grows tenfold, which component breaks first?

These are questions about performance and scalability, which Chapter 2 makes precise, and about caching and database choice, which Chapter 7 treats in depth. The next section turns from data to code, and to how code is organised so that people can understand and change it.

Check your understanding

Why does 0.1 + 0.2 not give exactly 0.3 in most languages?

Floating-point numbers are stored in binary, and many decimal fractions, including 0.1 and 0.2, have no exact binary form, so tiny rounding errors appear.

Which data structure suits an "undo" button in a text editor, and why?

A stack: each action is pushed on top, and undo pops the most recent one first (last in, first out).

If an O(n) process takes one second for 100,000 items, roughly how long for 1,000,000?

About ten seconds, because linear work grows in proportion to the data.

What do you pay for the speed a database index gives you?

Extra storage, and extra work on every insert, update and delete to keep the index up to date.

Next · 0.3Organising codeHow programmers divide code into functions, modules, objects and interfaces; the difference between a library and a framework; the benefits and risks of dependencies; and a first look at coupling and cohesion.Continue →

Sources and further reading

Have a question about this section? ArchitectBot, the course’s AI study assistant, answers from the course first.

Ask: “How do I tell whether a slow page in our app is caused by a missing database index or by something else?”