Be Software Architecture Wise
Be Wise Apps

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

Section 0.5 of 0.8 · Beginner · 18 min read

Files and databases: relational and non-relational storage, queries and transactions

Video lesson

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

A running program keeps its working data in memory, which forgets everything when the program stops. Anything that must survive, such as a customer's account or an order, has to be written to storage. Think of a database as a library with a careful librarian. It keeps a catalogue, checks every record follows the rules, and never loses a change it has promised to keep.

The simplest way to keep data is a file. Files are excellent for things that are naturally whole documents, such as images, PDF files, videos and logs. But imagine a shop keeping all its orders in one spreadsheet file. Two customers check out at the same moment. Whose change wins? The server crashes halfway through saving. Is the file now half old and half new? And someone types a customer number that doesn't exist. Who stops them? Every program using the file would have to solve each of these problems itself.

A database management system solves these problems once, for everyone. Applications, such as a shop's web app and a reporting tool, never touch the data files directly. They send requests, written in a language called SQL. The database management system checks the rules, plans how to answer each question, and manages many users at once. It writes the data to files on disk, and records every change, in order, in a log. And copies of both are sent regularly to backup storage in a separate location.

The most widely used kind is the relational database. Data lives in tables. Each table describes one kind of thing. Each row is one instance, such as one customer, and each column is one fact about it, such as their email. Each table has a primary key, a value that's unique for every row, such as the customer ID. An order holds a customer ID too. That's a foreign key: it points to a row in the customers table. The database will refuse an order for a customer who doesn't exist. Each fact is stored once. An order can contain many products, so a linking table, order items, records one row per product per order. Answering a question then means combining tables, which SQL does with a join.

How does the database find data quickly? Suppose an orders table has ten million rows, and a support screen needs the twenty orders for one customer. Without help, the database has to read every single row and check it. That's called a full table scan. An index works like the index at the back of a book. It's a sorted tree, only three or four levels deep, so the database reads a handful of pages and goes straight to the twenty rows. If the table grows tenfold, the scan does ten times the work. The index adds one more level at most. But indexes aren't free. Every new order must also update the index, and it takes space. It's that trade of space for speed again.

Moving a hundred pounds from Alice to Bob needs two changes. If the server crashes between them, the money vanishes. A transaction makes changes succeed together or not at all. Its guarantees are called ACID. Atomicity means all or nothing. Consistency means the rules are never left broken. A rule that balances can't go below zero rejects a transfer Alice can't afford. Isolation means two transfers running at once can't both rely on the same old balance. And durability means that when the app says transfer complete, the change has really been saved, and survives a crash or power cut.

Relational databases aren't the only option. NoSQL is an umbrella name for databases that don't use tables in the same way. Document databases store self-contained records, which suits something like a product catalogue. Key-value stores are giant dictionaries, for very fast lookups such as shopping baskets. Wide-column stores handle huge volumes of writes, such as sensor readings, spread across many machines. And graph databases answer questions about connections. Many real systems use more than one kind, each where it fits.

Disks fail and people make mistakes, so data needs backups kept somewhere separate. And a backup that has never been restored is a hope, not a backup. A replica, a second database that copies the first, protects against a failed machine. But a wrong delete reaches the replica within milliseconds. Finally, data usually outlives the code around it. Applications are rewritten every few years; customer records are kept for decades. That's why decisions about where data lives are some of the most expensive to reverse.

The full section has real SQL queries, a working bank transfer and more on the shop's schema. Next, in Section zero point six, we look at APIs, and how separate systems work together.

What you need to know

Why programs need somewhere to keep data

A running program keeps its working data in memory (RAM), which is fast but forgets everything when the program stops or the machine restarts (see what software is). Anything that must survive a restart, such as a customer's account, an order or a hospital appointment, has to be written to storage that keeps its contents without power: a disk or solid-state drive. Data that survives in this way is called persistent, and deciding how to persist it is one of the first and longest-lived decisions in any system.

The simplest way to persist data is a file: a named sequence of bytes that the operating system stores on disk. Files are excellent for things that are naturally whole documents: images, PDFs, videos, configuration, logs, exported reports. Systems often keep them in cloud object storage (a service that stores files by name) and record only a reference elsewhere.

Files become painful when many users read and change small pieces of structured data at once. Imagine a shop keeping all its orders in one spreadsheet file. Two customers check out at the same moment: whose change wins? The server crashes halfway through saving: is the file half old, half new? A support agent wants one customer's orders: must the program read the whole file? Someone types a customer number that does not exist: who stops them? Every program using the file would have to solve each problem, identically.

What a database is

A database is an organised collection of data, and the program that looks after it is a database management system (DBMS), such as PostgreSQL, MySQL, Microsoft SQL Server, Oracle Database or SQLite. People often say "the database" for both. The DBMS solves the file problems once, for everyone: it lets many users work at the same time without trampling each other, finds data quickly, refuses data that breaks the rules, and recovers to a correct state after a crash.

The analogy is a library. A heap of papers on a desk works if you are its only user. A library has a catalogue, rules about where things go, a record of who borrowed what, and a librarian who will not let two people take the same copy. Applications do not touch the shelves; they ask the librarian.

In most business systems the DBMS runs as its own server process, often on its own machine, and applications talk to it over the network (see how computers talk). SQLite is a well-known exception: it is a library that runs inside the application and stores the whole database in a single file, which is why it is found in phones, browsers and small tools.

Where the data actually lives

Applications send SQL to a database server, whose DBMS writes data files and a log on disk, which are copied to separate backup storageDatabase server machineShop web appreads and writesReporting toolreads onlySQLSQLDBMSchecks rulesplans queriesmanages transactionsData filestables and indexesTransaction logevery change, in orderBackup storageseparate locationregular copies
Applications never write the database's files directly: they send requests to the DBMS, which writes data files and a log, and a separate process copies both to backup storage.

Relational databases: tables, keys and relationships

The most widely used kind of database is the relational database, based on a model published by Edgar F. Codd of IBM in 1970. Data lives in tables. Each table describes one kind of thing (customers, orders, products). Each row is one instance of that thing (one customer), and each column is one fact about it (name, email address, date joined), with a declared type such as integer, text, date or decimal.

Each table has a primary key: a column (or combination of columns) whose value is unique for every row and never empty, such as customer_id. A foreign key is a column in one table that holds the primary key of a row in another table. An order has a customer_id column, which says which customer placed it. Declaring it as a foreign key tells the DBMS to refuse an order for customer 999 if there is no customer 999: this is called referential integrity.

The full set of tables, columns, types, keys and rules is the schema. The schema is a contract: every program that uses the database can rely on an order always having a valid customer and a price that is a number. Changing a schema later, when millions of rows and several programs depend on it, is possible but takes care, which is why schema evolution has its own section in Chapter 7.

Relational designs try to store each fact once. The customer's email address lives in one row of the customers table, not copied into every order. This is called normalisation. It means an update happens in one place and cannot leave contradictory copies behind. The price for this tidiness is that answering a question often means combining tables, which is what joins are for.

Asking questions with SQL

SQL (Structured Query Language, pronounced "S-Q-L" or "sequel") is the language used to work with relational databases. It was developed at IBM in the 1970s and became an ANSI and ISO standard in the late 1980s; every major relational DBMS speaks it, each with small dialect differences. SQL is declarative: you describe the result you want, not the steps to find it. The DBMS's query planner chooses the steps, for example whether to read a whole table or use an index.

A join combines rows from two or more tables by matching columns, usually a foreign key against the primary key it refers to. The second query below lists each line of a customer's orders together with the product's name, drawing on three tables at once.

-- One customer's orders since 1 October, newest first
SELECT order_id, status, placed_at
FROM orders
WHERE customer_id = 42
  AND placed_at >= '2026-10-01'
ORDER BY placed_at DESC;

-- Each order line with the product's name (a join)
SELECT o.order_id, p.name, i.quantity, i.unit_price
FROM orders AS o
JOIN order_items AS i ON i.order_id = o.order_id
JOIN products AS p ON p.product_id = i.product_id
WHERE o.customer_id = 42;
Two queries against the shop schema shown later in this section: a filtered SELECT and a three-table JOIN.

INSERT, UPDATE and DELETE change data, and CREATE TABLE defines the schema. Application code often sends SQL through an object-relational mapper (ORM), a library that turns rows into objects. ORMs save typing but hide the SQL actually sent; a classic performance problem is an ORM quietly issuing one query per row instead of one join.

Indexes: the book's index, not the whole book

Without help, the only way for the database to find every order for customer 42 is to read every row in the orders table and check each one: a full table scan, which is O(n) in the notation from data and algorithms. A database index is an extra, sorted data structure that maps values of a column to the rows that contain them, just as the index at the back of a book maps words to page numbers. Most relational databases use a B-tree by default, a wide, shallow, always-balanced tree that finds a value in O(log n) steps.

Primary keys are indexed automatically. Other indexes are added deliberately, for columns that queries filter, join or sort on: CREATE INDEX idx_orders_customer ON orders (customer_id);. Indexes are not free. Every insert, update or delete must also update each index on the table, and each index takes disk space and memory. A table with fifteen indexes reads quickly and writes slowly. Choosing indexes is a trade-off guided by the queries the system actually runs, which is an early example of the space-for-time trade-off that runs through all of architecture.

Worked example

Worked example: finding one customer's orders

A shop has 10 million rows in its orders table. The customer service screen asks for all orders for one customer, about 20 rows.

Without an indexFull table scan: the database reads and checks all 10,000,000 rows to find the 20 that match.
With a B-tree index on customer_idEach index page holds hundreds of keys, so the tree is only three or four levels deep. The database reads a handful of index pages, then fetches the 20 matching rows.
GrowthIf the table grows to 100 million rows, the scan does ten times the work; the index lookup adds at most one more level.
Cost paid elsewhereEvery new order now writes to the table and to the index. Disk use grows by the size of the index.

This is O(n) against O(log n) from Section 0.2: often the difference between a response in seconds and one in milliseconds. Many slow-query complaints in young systems are fixed by the right index, not bigger hardware.

Transactions and ACID: the bank transfer

Moving 100 pounds from Alice's account to Bob's needs two changes: subtract 100 from Alice, add 100 to Bob. If the server crashes after the first change and before the second, 100 pounds has vanished. If two transfers from Alice run at once, both might see a balance of 150 and both might succeed, leaving her overdrawn. A transaction groups several operations into one unit of work that either completes entirely (commit) or has no effect at all (rollback).

The guarantees a transaction gives are summarised as ACID, an acronym coined by Theo Härder and Andreas Reuter in 1983, building on earlier work by Jim Gray:

PropertyWhat it promisesIn the bank transfer
AtomicityAll the changes in the transaction happen, or none do.Alice is never debited without Bob being credited.
ConsistencyA transaction moves the database from one valid state to another; rules such as keys and checks are never left broken.A rule that balances may not go below zero rejects a transfer Alice cannot afford.
IsolationTransactions running at the same time do not see each other's half-finished work; the result is as if they ran one after another (to a degree set by the isolation level).Two simultaneous transfers from Alice cannot both rely on the same old balance.
DurabilityOnce the database confirms a commit, the change survives a crash or power cut.When the app says "Transfer complete", the money has really moved, because the change is already in the log on disk.
import sqlite3

conn = sqlite3.connect("bank.db")
# The accounts table was created with CHECK (balance >= 0)

def transfer(from_id, to_id, amount):
    try:
        with conn:  # one transaction: commit on success, rollback on error
            debit = conn.execute(
                "UPDATE accounts SET balance = balance - ? WHERE id = ?",
                (amount, from_id))
            credit = conn.execute(
                "UPDATE accounts SET balance = balance + ? WHERE id = ?",
                (amount, to_id))
            if debit.rowcount != 1 or credit.rowcount != 1:
                raise ValueError("No such account")  # forces a rollback
        return "Transfer complete"
    except (sqlite3.Error, ValueError):
        return "Transfer refused: no account was changed"
A transfer using Python's built-in sqlite3 module. The with block commits if both updates succeed and rolls back if either fails or an account does not exist.

Note the check on rowcount (the number of rows each update changed): an update that matches no account is not an error to the database, so the code must treat it as one itself. Two further details matter beyond this example. The ? placeholders pass values separately from the SQL text, which is the standard defence against SQL injection, an attack in which user input is smuggled into a query as code (Chapter 8 returns to this). And the durability promise is kept by a write-ahead log: the DBMS records each change in an append-only log on disk before confirming the commit, so after a crash it can replay or undo work to reach a correct state.

Isolation is the subtlest property. Full isolation (called serializable) is safe but limits how much work can run in parallel, so databases offer weaker isolation levels that trade some protection for speed, and many use a weaker level by default. Which anomalies each level allows is covered in transactions and isolation. For now, the key idea is that a single database gives strong, well-understood guarantees within one transaction, and those guarantees are much harder to obtain once data is spread across several databases or services.

NoSQL: other shapes of data

Relational databases are not the only option. NoSQL is an umbrella name, popularised at a 2009 meetup in San Francisco, for databases that do not use the relational model. They grew up to handle very large volumes spread across many machines, flexible data shapes, and access patterns that tables serve awkwardly. The name says what they are not, so it covers quite different families.

The four main NoSQL families

FamilyHow it stores dataGood fitExamples
Document databaseEach record is a self-contained document, usually JSON-like, which can nest lists and sub-objects. Documents in one collection need not share exactly the same fields.Data read and written as a whole: a product catalogue with varied attributes, a user profile, content for a web page.MongoDB, Couchbase, Firestore
Key-value storeA giant dictionary: a unique key maps to a value the database does not look inside. Lookup is by key only.Very fast simple lookups: sessions, shopping baskets, caches, feature flags.Redis, Amazon DynamoDB, etcd
Wide-column storeRows identified by a key, each with many columns, grouped and spread (partitioned) across many machines. Tables are designed around the queries that will be run.Huge volumes of writes, such as sensor readings, event logs and messaging history, across many servers and data centres.Apache Cassandra, Apache HBase, Google Bigtable
Graph databaseNodes (things) and edges (relationships), both with properties, as in the graphs of Section 0.2.Questions about connections: friends of friends, fraud rings, route planning, recommendations.Neo4j, Amazon Neptune

Practitioners disagree about the default. One camp says to start with a relational database unless there is a clear reason not to: SQL is widely known, transactions and joins are powerful, and modern relational systems also store JSON documents well. The other camp says to choose the store that matches the access pattern from the start, because a document or key-value store can be simpler and scale more cheaply for the right workload. Both agree that many real systems use more than one kind, each where it fits, which is called polyglot persistence. Chapter 7's section on choosing a database gives a structured way to decide.

Worked example: an online shop's schema

Consider a small online shop. It needs to know who its customers are, what it sells, and who ordered what. A first attempt might put everything in one table with a row per order and columns product1, product2, product3. That breaks as soon as someone orders four things, and it copies the customer's name and address into every order.

An online shop schema: orders refer to customers, and order items refer to both orders and productscustomerscustomer_id (PK)nameemailcreated_atordersorder_id (PK)customer_id (FK)statusplaced_atorder_itemsorder_id (PK, FK)product_id (PK, FK)quantityunit_priceproductsproduct_id (PK)namepricestockEach arrow runs from a foreign key (FK) to the primary key (PK) it refers to.
A normalised shop schema: each fact is stored once, and foreign keys record the relationships.

Worked example

Worked example: why the schema has four tables

The schema above answers the shop's questions without copying facts.

customersOne row per person. Changing an email address changes one row.
ordersOne row per checkout. customer_id is a foreign key, so every order belongs to a real customer. One customer has many orders: a one-to-many relationship.
productsOne row per item for sale, with the current price and stock level.
order_itemsAn order contains many products and a product appears in many orders: a many-to-many relationship. Relational databases represent it with a linking table, one row per product per order.
Deliberate copy: unit_priceThe price is copied into order_items when the order is placed. This is not a normalisation mistake: the order must record what the customer actually paid, even after the catalogue price changes next week.
Placing an orderInserting the order, inserting its items and reducing each product's stock happen in one transaction, so a crash can never leave an order without items or stock that was never reduced.

Good schema design starts from the questions the business asks and the facts it must never lose, not from the screens of the first version of the app.

The same shop in a document database might store each order as one document with its items nested inside, which suits reading a whole order at once but makes questions such as "total sales per product" harder.

Backups, recovery and managed databases

Disks fail, people run the wrong DELETE, software has bugs and attackers encrypt data for ransom. A backup is a copy of the data kept somewhere separate, from which the database can be restored. Three professional habits matter. A backup that has never been restored is a hope, not a backup, so teams rehearse and time restores. Copies are kept in more than one place (a common rule of thumb is three copies, on two kinds of media, one off-site). And a replica, a second database that continuously copies the first, protects against a machine failing but not against mistakes: a bad DELETE reaches the replica within milliseconds. Many databases can combine a periodic full backup with the saved transaction log to restore to any moment, called point-in-time recovery.

Running a database well (patching, tuning, replicating, backing up and restoring at three in the morning) is skilled, never-ending work, so cloud providers sell a managed database: Amazon RDS, Azure SQL Database and Google Cloud SQL are examples. The provider handles the machines, software updates, automated backups and failover to a standby copy; the customer still owns the schema, the queries, the indexes, access permissions and the cost. Managed services are covered more broadly in servers and the cloud.

Why this matters for architecture

Data usually outlives the code around it: applications are rewritten every few years, while customer records and medical histories are kept for decades. Decisions about where data lives, which kind of database holds it, which program owns it and which guarantees it needs are therefore some of the most expensive to reverse. Several big architectural questions grow directly from this section: whether each service should own its own database (data ownership), which database fits which job (choosing a database), and how to keep data correct when one business action touches several databases, where a single ACID transaction is no longer available (sagas). When an architect asks "where is the source of truth for this?", this section is the reason.

Check your understanding

Why is a spreadsheet file a poor place to keep an online shop's orders once many customers are buying at once?

Simultaneous writers can overwrite each other, a crash mid-save can corrupt the file, finding one customer's orders means reading everything, and nothing enforces rules such as every order having a real customer. A DBMS solves all four.

In the shop schema, what makes customer_id in the orders table a foreign key, and what does that buy you?

It holds the primary key of a row in the customers table and is declared as a reference to it. The database then refuses orders for customers that do not exist.

Why not simply index every column of every table?

Each index must be updated on every insert, update and delete, and takes space. Too many indexes make writes slow and storage expensive, so indexes are chosen to match the queries actually run.

Which ACID property is broken if a crash leaves Alice debited but Bob not credited?

Atomicity: the two changes should have happened together or not at all.

A team says "we have a replica in another data centre, so we do not need backups". What have they missed?

A replica copies mistakes and corruption as faithfully as good data, so an accidental delete or a ransomware attack reaches it within moments. Backups, ideally with point-in-time recovery, protect against that.

Next · 0.6APIs and integrationWhat an API is and why it is a contract, how REST APIs use URLs, HTTP verbs, status codes and JSON, and how keys, webhooks, queues, versions and rate limits shape integration.Continue →

Sources and further reading

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

Ask: “For a small online shop, how do I decide between a relational database like PostgreSQL and a document database like MongoDB?”