System DesignBeginner 13 min Lesson 11 of 42

Database — Where the Truth Lives

Why applications need a database rather than files, what transactions guarantee, and why connection pooling matters.

System Design · Lesson 11 of 42
0/42 done(0%)

What is it? #

A database stores data in a structured way and answers questions about it quickly, safely and concurrently.

Files can store data too. What they cannot do well is let twenty processes write at once without corrupting each other, find one record among ten million instantly, or guarantee that a half-finished update leaves nothing behind.

Relational databases organise data into tables with defined columns and relationships between them. Constraints — not null, unique, foreign keys — push correctness into the database itself, so no application bug can create impossible data.

Transactions are the other core feature: a group of changes that either all apply or none do.

Think of it like this #

A bank ledger with strict rules, compared with a shared notebook.

The notebook works until two people write at once, or someone tears out a page, or a transfer is written on one line and the matching line is forgotten. The ledger enforces that both sides of a transfer are recorded together or neither is.

Simple example #

Transferring money between two accounts means two updates: one decrease, one increase. If the second fails, the first must be undone. A transaction is what makes that guaranteed.

Code #

SQL
-- Structure with constraints: the database enforces correctness
CREATE TABLE accounts (
    id         INTEGER PRIMARY KEY,
    owner      TEXT    NOT NULL,
    balance    NUMERIC NOT NULL CHECK (balance >= 0),
    created_at TIMESTAMP NOT NULL DEFAULT now()
);

CREATE TABLE transfers (
    id        INTEGER PRIMARY KEY,
    from_id   INTEGER NOT NULL REFERENCES accounts(id),
    to_id     INTEGER NOT NULL REFERENCES accounts(id),
    amount    NUMERIC NOT NULL CHECK (amount > 0),
    made_at   TIMESTAMP NOT NULL DEFAULT now()
);

-- A transaction: both updates apply, or neither does
BEGIN;
  UPDATE accounts SET balance = balance - 500 WHERE id = 1;
  UPDATE accounts SET balance = balance + 500 WHERE id = 2;
  INSERT INTO transfers (from_id, to_id, amount) VALUES (1, 2, 500);
COMMIT;
-- If any statement fails, ROLLBACK undoes everything in the block
PYTHON
# Connection pooling: reuse connections instead of opening one per request
from psycopg_pool import ConnectionPool

pool = ConnectionPool("postgresql://app:secret@localhost/shop", min_size=2, max_size=10)


def transfer(from_id: int, to_id: int, amount: float) -> None:
    with pool.connection() as conn:          # borrowed, returned automatically
        with conn.transaction():             # commits, or rolls back on error
            conn.execute(
                "UPDATE accounts SET balance = balance - %s WHERE id = %s",
                (amount, from_id),
            )
            conn.execute(
                "UPDATE accounts SET balance = balance + %s WHERE id = %s",
                (amount, to_id),
            )
TEXT
ACID, in plain words

Atomic       all the changes in a transaction happen, or none do
Consistent   constraints are never violated, before or after
Isolated     concurrent transactions do not see each other's half-done work
Durable      once committed, it survives a crash or power loss

How it works #

The CHECK (balance >= 0) constraint means an overdraft cannot exist in the table, even if an application bug tries. Constraints are the last line of defence and the only one that applies to every client, including manual queries.

REFERENCES accounts(id) is a foreign key. It prevents a transfer row pointing at an account that does not exist, and it stops that account being deleted while transfers reference it.

BEGIN and COMMIT bracket the transaction. Between them, other transactions do not see the partial state. If anything fails, ROLLBACK returns the database to exactly how it was.

The Python example uses a connection pool. Opening a database connection is expensive — often tens of milliseconds — so a pool keeps a few open and lends them out. Without one, a busy application spends most of its time connecting.

max_size=10 matters more than it looks. Databases have a connection limit, and each connection consumes memory. Ten application servers with a pool of 100 each will exhaust a typical PostgreSQL instance.

with conn.transaction(): commits on a clean exit and rolls back on an exception, which is the safe default and removes a whole category of "forgot to commit" bugs.

Real-world use #

Almost every application has a database at its centre, and it is usually the hardest component to scale, because it is the one place where state lives.

That is why caches, read replicas and queues exist: each removes load from the database. It is also why the database is the most common cause of slow endpoints, and where performance work starts.

Transactions are used far beyond money. Creating an order with its line items, updating stock while recording a reservation, or registering a user while creating their default settings all need all-or-nothing behaviour.

Connection limits cause real outages. An autoscaling application that opens a new pool per instance can exhaust the database during a traffic spike — exactly when you need it most. Connection poolers such as PgBouncer exist for this reason.

Common mistakes #

  • Skipping constraints and relying on application code to keep data valid.
  • Opening a new connection per request instead of using a pool.
  • Pool sizes that multiply across instances and exhaust the database limit.
  • Holding a transaction open while calling an external API, blocking other writers.
  • Running many small queries in a loop instead of one query.

Practice #

Create two tables with a foreign key and a check constraint. Write a transaction that inserts into both, then deliberately violate the constraint in the second statement and confirm the first is rolled back. Finally, time 100 operations with a fresh connection each versus a pooled connection.

Quick quiz

  1. 1. What does a transaction guarantee?

  2. 2. Why use database constraints as well as application validation?

  3. 3. What problem does connection pooling solve?

  4. 4. Why is an oversized connection pool dangerous?

  5. 5. What does the "D" in ACID mean?

Summary

  • A database gives structure, fast lookups, concurrency safety and transactions.
  • Constraints enforce correctness for every client, not just your code.
  • Transactions make multi-step changes all-or-nothing.
  • Use a connection pool, and size it with the database limit in mind.
  • The database is usually the hardest part to scale, so protect it.