Why Software Architecture Matters

Software Architecture Fundamentals

Chapter 1 · Why Software Architecture Matters

This course is a direct sequel to Design Patterns — but it operates at a genuinely different scale. A pattern solves a local problem: how two or three objects collaborate. Architecture solves a system-shape problem: how an entire codebase is organized, and — the specific thing this chapter measures — how expensive a given decision is to change once the system has grown around it.

Architecture vs. Design vs. Implementation

These three terms get used loosely, but they describe genuinely different scopes of decision, distinguished by the same question at every level: how much of the codebase does changing your mind touch?

LevelExample decisionTypical blast radius of changing it
ImplementationUsing a for loop instead of a list comprehension inside one functionOne function
Design (patterns)Which discount Strategy object an Order currently holdsOne line — verified below
ArchitectureWhether business logic talks to storage directly, or through one boundaryEvery function that touches storage — verified below

A Local Decision, Verified Cheap to Change

Design Patterns Chapter 7 built exactly this kind of local decision: an Order holding a swappable shipping_strategy. That chapter verified something directly relevant here — swapping StandardShipping for ExpressShipping on an already-created Order object took one line (order.set_shipping_strategy(ExpressShipping())), and every other part of the codebase was completely unaffected. That's a design-level decision: cheap, local, reversible.

This chapter measures the opposite case — a decision made at the architecture level, where getting the boundary wrong makes an otherwise-simple change expensive.

An Architectural Decision, Measured

Without a Boundary: Storage Woven Directly Into Business Logic

def record_order(order_id, total): with open('orders.txt', 'a') as f: f.write(f'{order_id},{total}\n') def record_payment(order_id, amount): with open('payments.txt', 'a') as f: f.write(f'{order_id},{amount}\n') def generate_report(): with open('orders.txt') as f: return [line.strip() for line in f]
Verified directly — every business function is coupled to the specific storage technology
Scanning the source of all 3 functions for the literal string open( (Python's own file-open call) finds it in 3 of 3 — every single function. There is no single place in this codebase that "does storage" — the decision to use flat text files is scattered across every function that happens to need persistence.

With a Boundary: One Repository, Everything Else Unchanged

class FileOrderRepository: def save_order(self, order_id, total): with open('orders.txt', 'a') as f: f.write(f'{order_id},{total}\n') def save_payment(self, order_id, amount): with open('payments.txt', 'a') as f: f.write(f'{order_id},{amount}\n') def get_orders(self): with open('orders.txt') as f: return [line.strip() for line in f] # the business functions now depend on an abstraction, not a file def record_order(repo, order_id, total): repo.save_order(order_id, total) def record_payment(repo, order_id, amount): repo.save_payment(order_id, amount) def generate_report(repo): return repo.get_orders()
Verified directly — zero business functions mention storage at all
Scanning the same 3 business functions for open( now finds it in 0 of 3. All storage-specific code lives in exactly one place: FileOrderRepository.
Verified directly — swapping the actual storage technology touches zero business-function source code
Writing a second repository, InMemoryOrderRepository (same three methods, backed by a Python list instead of a file), and injecting it in place of FileOrderRepository: both repositories were captured via Python's own inspect.getsource() before and after the swap. The three business functions' own source text was confirmed character-for-character identical before and after — record_order, record_payment, and generate_report were never opened, let alone edited. Calling generate_report(file_repo) and generate_report(memory_repo) with the same recorded order both correctly returned ['ORD-1,100'].

Why This Gets Worse, Not Better, as the Codebase Grows

Verified directly — the boundary-free version's cost scales 1:1 with the number of functions
Extending the boundary-free version from 3 functions to 6 (adding record_refund, generate_payment_report, generate_refund_report, each with their own direct open() call) produced 6 of 6 functions needing a touch to swap storage — the same 1:1 ratio as the original 3-function version. The boundary-with version's own cost stays flat at 0 business-function touches regardless of how many functions call the repository — only the one-time cost of writing a new repository class changes.
This is the actual definition this chapter is building toward
An architectural decision isn't "important-sounding" or "made by a senior engineer" — it's specifically a decision whose cost of changing your mind grows with the size of the codebase, unless a boundary was deliberately put in its way. The persistence boundary above (a Repository) is one instance of a much more general idea this course returns to in every chapter: Layered Architecture (Chapter 2) and Hexagonal Architecture (Chapter 7) are both, at heart, systematic ways of making sure this kind of boundary exists before you need it, not after.

Where This Connects

This chapter's findingWhat it sets up
Design Patterns Chapter 7's one-line Strategy swap, reused directly as the "cheap" baselineEvery pattern in that course operates at this same low-cost, local scale — architecture is the layer above it, not a replacement for it
The Repository boundary keeping business logic at 0 touchesChapter 2's Layered Architecture generalizes this one boundary into a full, named set of layers
An unexamined decision (no boundary) costing 1 touch per function, foreverChapter 9's Architectural Decision Records exist specifically to make a decision like "do we need a boundary here" deliberate and recorded, not accidental

Hands-On Exercises

Exercise 1

Add a fourth boundary-free function, record_refund(order_id, amount), to this chapter's own first (no-repository) example, using the same direct open() pattern. Verify the touch count is now 4 of 4.

📄 View solution
Exercise 2

Add a matching save_refund(order_id, amount) method to this chapter's own FileOrderRepository and InMemoryOrderRepository, plus a new business function record_refund(repo, order_id, amount) that calls it. Verify this new function's source contains no open( call, and that it works correctly against both repositories.

📄 View solution
Exercise 3

Using this chapter's own compare-table (Implementation / Design / Architecture), classify each of the following as one of the three levels, and justify your answer using this chapter's own "how much of the codebase does changing your mind touch?" test: (a) renaming a local variable inside one function, (b) switching an Order's discount strategy at runtime, (c) deciding whether a system is one monolith or split into several services.

📄 View solution

Chapter 1 Quick Reference

  • The test: a decision's level (implementation / design / architecture) is measured by how much of the codebase changing your mind touches — not by how important it sounds
  • Verified: a design-level decision (Design Patterns' own Strategy swap) cost 1 line; an architecture-level decision made without a boundary cost 3 of 3 (then 6 of 6) function touches; the identical decision made with one boundary (a Repository) cost 0 business-function touches, confirmed via character-identical source text before and after the swap
  • Next chapter: Layered Architecture — generalizing this one boundary into a full, named set of layers