Software Design Reference Core Designer · Structuring for Stability

Lesson 0006 · Core Designer · Module 2

Design Around Reasons to Change

Responsibilities are fuzzy; change sources are facts. Structure your modules around the forces that will actually edit them.

Mission tie-in: "design around reasons to change" is the upgrade from lesson 0002's SRP. It turns a principle into a method you can apply before writing a line.

Knowledge: the change source is the unit

Every future edit to your codebase comes from a finite list of forces: tax law changes, a new payment provider, a new report format, a re-branding. Call each one a reason to change. Design says: code that changes for the same reason lives together; code that changes for different reasons lives apart.

Consider an invoice class:

class Invoice:
    def __init__(self, lines: list[Line], vat_rate: Decimal) -> None:
        self.lines = lines
        self.vat_rate = vat_rate

    def total(self) -> Money:
        subtotal = sum(line.price for line in self.lines)
        return Money(subtotal * (1 + self.vat_rate))

    def to_html(self) -> str:
        rows = "".join(f"<tr><td>{l.name}</td></tr>" for l in self.lines)
        return f"<table>{rows}</table>"

    def to_json(self) -> dict:
        return {"lines": [{"name": l.name, "price": str(l.price)} for l in self.lines]}

Two reasons to change are welded together: how invoices are calculated (tax rules, discounts) and how invoices are rendered (HTML tweaks, new JSON fields for a mobile client). When the mobile client needs a new field, you edit the class that owns the tax calculation — and risk the tax rules.

The stable design separates them:

class Invoice:
    def __init__(self, lines: list[Line], vat_rate: Decimal) -> None:
        self.lines = lines
        self.vat_rate = vat_rate

    def total(self) -> Money:              # reason: pricing rules
        subtotal = sum(line.price for line in self.lines)
        return Money(subtotal * (1 + self.vat_rate))

class InvoiceRenderer:
    def __init__(self, invoice: Invoice) -> None: ...
    def to_html(self) -> str: ...          # reason: web presentation
    def to_json(self) -> dict: ...         # reason: API contract

Now a pricing change edits Invoice and nothing else; a rendering change edits InvoiceRenderer and nothing else. Note what you did not do: you didn't count methods, and you didn't split for symmetry. You named the change sources — the whole method.

To find a module's reasons to change, ask "which parts of the world would make me edit this file?" Each answer that names a different part of the world is a different reason.
Field notes · what it looks like in real code
You see thisWhat it costsThe move
git log --format=%s -- billing.pyThe commit subjects name this file’s change sources out loud.Group the subjects; each distinct world is a module.
Invoice.to_html() beside Invoice.total()A branding tweak edits the file that owns the tax rules.Move rendering out; the calculation stops being touched.
one class edited by three teamsThree worlds, one file — every change becomes a negotiation.Split along the team boundary (lesson 0033).
a helpers.py nobody claimsIts change source was never named, so nobody owns its edits.Ask who edits it and why; that answer is its home.

Skill: name the change sources

The invoice class above should be split because:

A class with many methods and one change source is:

To discover a module's reasons to change, ask:

Practice on your own code

Pick your most-edited class. List the world events that have actually forced edits to it in the last year (a law, a vendor, a format, a team). If the list names two or more distinct worlds, you have found a split that would have paid for itself.

Reveal: a worked split

A ReportGenerator that both queries the warehouse and formats PDFs. Edit history: new warehouse schema (twice) and new branding (once). Two worlds. Split into WarehouseReportQuery (owned by the warehouse team's contract) and ReportFormatter (owned by design). Branding changes stop touching query code.

Your win

You can now enumerate a module's reasons to change from its edit history — and split along those lines instead of along method counts or symmetry.

Read and watch deeper

Bring a class you edit often to your agent-teacher and co-list its change sources — disagreement is where the insight lives.