Software Architecture / Foundations: promises, structure, and ownership / Lesson 4
Design modules around change and dependency rules
Build cohesive booking modules and keep external delivery mechanisms outside business policy.
On this page
Lesson goals
- Recognize coupling by the changes that must move together.
- Use explicit interfaces to keep policy independent from replaceable mechanisms.
Group knowledge that changes for the same reason
A module is a boundary for responsibility and knowledge, not merely a folder. Reservation rules belong together because seat capacity, cancellation eligibility, and confirmation state must agree. Mail formatting changes for a different reason and need not be mixed into seat allocation. Cohesion is contextual: two operations that share a database table may still represent different policies, while several tables may support one business invariant. Examine concrete changes and failure ownership before deciding where a module begins or how large it should be.
Find the dependency that forces coordinated edits
Coupling becomes visible when changing one module requires understanding or editing another. A booking policy that imports a mail-vendor exception type depends on details of the delivery mechanism. A shared utility package can also become a hidden hub if every business module imports its unstable types. Count imports only as clues; semantic coupling can exist through a database schema, a file format, or an undocumented timing expectation. A useful review asks what would happen if one collaborator changed independently, not whether the directory tree looks tidy.
booking_policy -> reservation_port
booking_application -> notification_port
sql_reservation_adapter -> reservation_port
mail_vendor_adapter -> notification_port
Arrows mean source-level dependency on an interface. The runtime application still calls concrete adapters after composition; dependency direction and runtime call direction are different ideas.
Put the interface in the language of the caller
A reservation port should express operations the business needs, such as reserve a seat atomically, rather than exposing arbitrary SQL execution. The adapter translates that operation into the storage technology. This preserves a meaningful contract when the mechanism changes, but an interface does not erase distributed failure or consistency limitations. Its return values must still distinguish confirmed, rejected, and unknown outcomes where relevant. Avoid creating one abstraction for every class; introduce a boundary when it isolates a real change, testing need, or external dependency.
Test policy without contacting an external provider
This complete Python model injects a delivery function into a small application function. It demonstrates a local seam rather than a full architecture framework. The policy rejects zero seats before delivery and sends one message for a positive request. A list records the call, so the test has no network, account, or mail side effect. Real reservation persistence is deliberately absent: the model cannot claim atomic seat allocation, durable retries, or message delivery. Its small scope makes the demonstrated dependency easy to inspect.
def notify_reservation(seats, deliver):
if seats < 1:
return "rejected"
deliver("reservation accepted")
return "accepted"
messages = []
print(notify_reservation(1, messages.append))
print(notify_reservation(0, messages.append))
print(len(messages))
accepted
rejected
1
The function returns accepted then rejected, and the list contains exactly one message. The names represent a teaching policy; no actual reservation is persisted.
Keep dependency rules visible after the first design
Document allowed imports and public entry points, then choose a proportionate enforcement method. Small projects may use review and focused tests; larger codebases can add static dependency checks. Keep the rule understandable to maintainers and allow justified exceptions with owners and removal plans. A module that every caller bypasses through direct database access is not encapsulated merely because an interface exists. Measure whether representative changes stay within their intended boundary and update the design when repeated exceptions reveal a mistaken responsibility split.
Common errors and fixes
Business policy imports a vendor mail client.
- Why it happens
- Mechanism details crossed the policy boundary.
- How to fix it
- Expose a caller-oriented notification port and implement a vendor adapter.
- Prevent it next time
- Review imports and exception types at external boundaries.
Every class gains an interface without a change scenario.
- Why it happens
- Abstraction count became a goal by itself.
- How to fix it
- Keep boundaries that isolate actual uncertainty and simplify others.
- Prevent it next time
- State the replacement or testing need for each abstraction.
Modules share unrestricted writes to each otherโs tables.
- Why it happens
- Folder separation was mistaken for ownership.
- How to fix it
- Define the owning write path and migrate bypasses deliberately.
- Prevent it next time
- Include data access in dependency reviews.
Check your understanding
1. A new mail provider requires changes to seat-allocation tests. What should you inspect?
Show worked solution
Look for vendor types, formatting assumptions, and delivery exceptions leaking into the reservation policy. Keep policy assertions about seats separate from adapter assertions about the provider request. Some application-level changes may still be legitimate if delivery semantics actually changed. The goal is to isolate mechanism changes while preserving meaningful behavior, not to force an unchanged test count.
2. Why does the injected-function example not prove reliable notification?
Show worked solution
The list is in process memory and disappears after execution. There is no durable reservation, transactional message record, retry policy, or external acknowledgement. The example proves only the local dependency seam and stated calls. A reliable implementation requires additional contracts and tests at persistence and delivery boundaries.
Key takeaways
- Module boundaries should isolate knowledge and change.
- An interface supports a contract; it does not manufacture operational guarantees.