Foundations: Python, Git and toolsLoading progress…

Project boundaries

Build a small HTTP service with clear boundaries, tests, logs and a repeatable runtime.

Why code needs boundaries

Consider one function that reads a JSON request, loads a discount from a database, calculates a price, saves the order and sends an email. To test the price you need a database and a mail server. To change the email provider you edit the pricing function. When something fails you cannot tell which part is to blame.

The fix is to keep decisions (calculations, rules) apart from effects (network, disk, database, clock). The rules become plain functions of plain values: easy to test, easy to reuse. Thin adapters translate between the outside world and those plain values.

# tangled: needs a database to test the maths
def price(order_id):
    row = db.query("select ...", order_id)
    return sum(q * p for q, p in row.items) * (100 - row.pct) // 100

# separated: the maths takes values; the adapter fetches them
def price(items, pct):
    return sum(q * p for q, p in items) * (100 - pct) // 100

Dependencies point one way

Each file that imports another depends on it. Draw an arrow for every import and you get a graph. Two rules keep it healthy: arrows point from the outside (web, adapters) toward the middle (domain), and there are no cycles. A cycle means A cannot be understood without B and B not without A; in Python it can also fail at import time with a partially initialised module.

webdomainstoragenotify
  • broken No import cycles between modules
    cycle through web, domain
  • broken Every import points the allowed way (web, then adapters, then domain)
    domain imports web (Order uses calc_subtotal); domain imports storage (apply_discount uses load_discount_rate)
  • broken Pure calculations and plain data live in domain
    not in domain: calc_subtotal, add_tax, format_money
  • broken Each I/O piece lives in its own adapter (web, storage, notify)
    misplaced: save_order, send_receipt
Same project, two layouts. Red arrows are imports that go the wrong way or form a cycle.

Predict first. Which is easier to test: price(items), or a function that also opens a database and sends email?

Breaking a cycle

The usual cause of a cycle is a low-level piece reaching up to something higher: a pricing rule that calls the database itself. The cure is passing the value in: the caller fetches the rate and hands it over, so the rule no longer needs to know a database exists. The arrow disappears instead of just moving.

Chapter context

Engineering makes a program understandable when it fails or changes. Separate pure calculations from external I/O so behavior can be checked cheaply and errors can be connected to specific inputs.

Chapter reference: math, worked example & code

This section belongs to the whole chapter, so it is the same on every lesson in it and may cover a different lesson's topic. The code for the experiment you just ran is inside the experiment itself.

Math and notation

For money examples, use integer minor units rather than binary floating point. The subtotal is sum(quantity × unit_price), and the domain must specify rounding and tax rules before those operations are implemented.

Worked example

Two items cost 125 minor units each and three cost 40 each. The subtotal is 2×125 + 3×40 = 370. A test can exercise this calculation without starting an HTTP server. Request parsing and response formatting need separate checks.

Reference code

The following example uses the Python standard library and does not call a remote service.

def subtotal(items):
    return sum(quantity * price for quantity, price in items)
assert subtotal([(2, 125), (3, 40)]) == 370
assert subtotal([]) == 0

Python examples are provided for study and local execution; this page does not execute Python.

Common mistakes

Test coverage counts executed lines, not verified requirements. A container does not turn an unsafe operation into a safe one. JSON validity alone does not establish application validity.