Exercises — Separation of concerns
Two numbers show up again and again, so let us pin them down in plain words before using them:
See Coupling and Cohesion for the full treatment; here we only use them.
Level 1 — Recognition
L1·Q1 — Name the concerns
Below, each commented line does something. List the distinct concerns (reasons to change), and how many there are.
def handle_order():
row = db.query("SELECT price FROM items WHERE id=5") # (a)
cost = row.price * 1.18 # (b)
open("log.txt","a").write("order handled\n") # (c)
print(f"You owe ${cost:.2f}") # (d)Recall Solution
- (a) Storage / persistence — changes if the database changes.
- (b) Business rule — changes if the tax rate or pricing law changes.
- (c) Logging — a cross-cutting concern; changes if logging policy changes.
- (d) Presentation — changes if the output format changes.
Distinct concerns = 4. Four different people, four different reasons, all tangled in one function. That is exactly the "one person does every kitchen job" anti-pattern.
L1·Q2 — Coupling by counting arrows
Module Report imports and calls Database, Formatter, and Mailer. Nothing else. What is Coupling(Report)?
Recall Solution
Coupling = number of other modules depended on = 3 (Database, Formatter, Mailer). That is the whole definition — count the arrows leaving the box. See the figure below.

Level 2 — Application
L2·Q1 — Split the tangle
Take the L1·Q1 function and rewrite it so each concern lives in its own function, composed at the end. Write the four functions.
Recall Solution
def fetch_price(item_id): # storage concern
return db.query(f"SELECT price FROM items WHERE id={item_id}").price
def total_with_tax(price, tax=0.18): # business concern
return price * (1 + tax)
def log_event(msg): # logging (cross-cutting)
open("log.txt","a").write(msg + "\n")
def render(amount): # presentation concern
return f"You owe ${amount:.2f}"
# composition:
log_event("order handled")
print(render(total_with_tax(fetch_price(5))))Why this split? A tax change now touches only total_with_tax. A database swap touches only fetch_price. Each concern has one home — the definition of SoC.
L2·Q2 — Tax edit blast radius
In the tangled version, a tax-law change forces you to edit how many functions? In the separated version, how many?
Recall Solution
- Tangled: the whole
handle_orderis one function, so 1 function is edited — but that same edit sits next to storage and presentation, risking them. - Separated: exactly 1 function,
total_with_tax, and it touches nothing else. The count is the same (1) but the isolation is the win: no storage/UI code shares the room.
The real metric is not "how many functions" but "how many concerns can this edit accidentally break": 3 vs 0.
L2·Q3 — Which concern is cross-cutting?
Of the four concerns in L2·Q1, one naturally appears in every function of a bigger app. Which, and what tool isolates it without pasting it everywhere?
Recall Solution
Logging is the cross-cutting concern. Isolate it with a decorator (or middleware), so business functions stay clean:
def logged(fn):
def wrap(*a, **k):
log_event(f"calling {fn.__name__}")
return fn(*a, **k)
return wrap
@logged
def total_with_tax(price, tax=0.18): ...The math and the logging run together but live physically apart.
Level 3 — Analysis
L3·Q1 — Score two designs
For a small script, compare:
- Design A: 1 module doing storage + business + UI.
- Design B: 3 modules (storage, business, UI); business depends on storage, UI depends on business.
Give Coupling of each module and a one-line verdict on cohesion.
Recall Solution
Design A: one module, so no other module to depend on → Coupling = 0. But it mixes 3 concerns, so cohesion is low (elements serve different reasons).
Design B:
storage: depends on nothing →Coupling = 0.business: depends onstorage→Coupling = 1.UI: depends onbusiness→Coupling = 1. Each module serves one concern → cohesion high.
Verdict: A wins the naive "zero coupling" contest but loses cohesion; B has tiny coupling (0,1,1) and high cohesion. This is why you optimise both, not one — see Coupling and Cohesion.
L3·Q2 — The over-split trap
A developer splits business into 6 nano-modules that all call each other in a ring: each depends on 2 neighbours. Total pairwise dependency arrows?
Recall Solution
A ring of 6 nodes, each pointing to its 2 neighbours → directed dependencies. Compared with Design B's total of arrows for the same work. This explosion is fragmentation: "high cohesion per tiny box" bought with runaway coupling. Splitting past the natural seam makes things worse.

L3·Q3 — Direction of arrows
In Layered Architecture, presentation → domain → data (one-way). Why does making the arrow point both ways (data also imports presentation) destroy SoC?
Recall Solution
A cycle means the database code now knows about the screen. Swap the UI and the data layer breaks; swap the database and the UI breaks. Concerns that were meant to change independently are now chained together — coupling becomes bidirectional, the blast radius of any change grows, and you can no longer test one layer alone. One-way arrows are SoC made structural; a cycle undoes it.
Level 4 — Synthesis
L4·Q1 — Design a notifications feature
You must add "send an email and an SMS when an order ships", with logging on every send. Sketch the modules and the dependency arrows so that adding a third channel (push) later touches minimal code.
Recall Solution
Separate the what (an order shipped) from the how (each channel), and treat logging as cross-cutting.
OrderService --emits--> ShipEvent
Notifier (loops over channels) --> [EmailChannel, SmsChannel]
logged --wraps--> every channel.send()
Notifierdepends on an interfaceChannel.send(msg), not on concrete classes (Information Hiding).- Adding push = write one new
PushChanneland register it.Notifier,OrderService, logging, email, SMS are all untouched. - Blast radius of a new channel = 1 new module, 0 edits to existing ones. That is the Single Responsibility Principle and SoC paying off together.
L4·Q2 — Where does tax belong?
Prices, currency formatting, and tax rate all appear. Assign each to a layer of Layered Architecture and justify by "reason to change".
Recall Solution
- Tax rate & price math → domain layer (business rules; changes when the law/pricing changes).
- Currency formatting (
$,.2f, commas) → presentation layer (changes when locale/UI changes). - Reading the stored price → data layer (changes when the DB changes).
Each sits in the layer whose reason to change it shares. If tax formatting crept into the data layer, a UI locale change would force a database-layer edit — a scatter.
Level 5 — Mastery
L5·Q1 — Cohesion as a fraction
Recall . A module has 4 elements. The number of possible unordered pairs is . If all pairs serve the same concern, what is the cohesion value? If only 2 of those pairs do?
Recall Solution
Total possible pairs .
- All 6 serve one concern → cohesion (perfectly cohesive).
- Only 2 do → cohesion (low; the module mixes concerns).
The fraction ranges from (nothing shares a purpose) to (everything does).
L5·Q2 — Degenerate edge cases
Evaluate cohesion and coupling for the extreme inputs. (a) A module with 1 element. (b) A module depending on 0 others. (c) A single giant module containing the whole program.
Recall Solution
- (a) 1 element: possible pairs , so cohesion is — undefined; by convention a one-element module is treated as trivially cohesive (there is nothing to disagree). Coupling depends only on its imports.
- (b) 0 dependencies:
Coupling = 0— the ideal for a leaf module likestoragein L3·Q1. It can be tested in total isolation. - (c) whole program in one module:
Coupling = 0(no other module), yet cohesion collapses toward its minimum because unrelated concerns share the box. This is precisely why coupling alone is a gameable metric — the degenerate case exposes it.
L5·Q3 — Prove the both-metrics claim
Show, using the numbers from L3, that neither "minimise coupling alone" nor "maximise cohesion alone" recovers a good design, but "cut along concern seams" does.
Recall Solution
- Minimise coupling alone → Design A: total coupling , but cohesion low (mixes 3 concerns). Optimum of the wrong objective.
- Maximise cohesion alone → L3·Q2 ring: each nano-module is "about one thing" (looks maximally cohesive) but total coupling . Optimum of the other wrong objective.
- Cut along seams (Design B): total coupling and each module fully cohesive. Strictly better than both extremes on the joint goal.
So SoC is not "minimise X"; it is find the partition that is simultaneously low-coupling and high-cohesion, which is the partition along real reasons to change.
Active recall
Recall The single fastest test for "is this well-separated?"
Ask: for a typical change, how many places must I edit? Good SoC → ideally one, and that edit cannot break unrelated concerns.
Recall Why is "coupling = 0" not automatically good?
Because one giant module also has coupling 0 (no other module exists) while mixing every concern. Coupling alone is gameable; pair it with high cohesion.