Lesson 0022 · System Designer · Module 2
Organizing Modules and Folders Around Boundaries
Folders are the interface your team reads first. Organize by responsibility and boundary — not by file type — and the architecture announces itself in the tree.
Mission tie-in: "organize modules and folders around responsibilities and boundaries" — the layout that keeps a growing application navigable and its dependency arrows visible.
Knowledge: the tree should tell the story
A project organized by file type — models/, services/, controllers/, utils/ — tells you nothing about what the system does. A project organized by boundary — domain, application services, infrastructure, delivery — tells you how change flows before you open a file:
checkout/
├── domain/ # the core: rules, value objects, aggregates (nothing imports out)
│ ├── order.py
│ ├── money.py
│ └── pricing.py
├── application/ # use cases: orchestrate domain via ports
│ ├── checkout_service.py
│ └── ports.py # PaymentGateway, OrderRepository, Inventory…
├── infrastructure/ # adapters: the world conforming to the ports
│ ├── stripe_adapter.py
│ ├── sqlalchemy_repository.py
│ └── email_adapter.py
└── api/ # delivery: HTTP, CLI, whatever the outside speaks
└── endpoints.py
The rules that keep it stable:
- Dependency direction is visible in the tree:
domainimports nothing outside itself;applicationimportsdomain;infrastructureimports both;apiimports application. A cycle shows up as an import you can't explain. - Naming by role, not by technology: "stripe_adapter" says what it is; "payments" as a sibling folder to "domain" says nothing.
- No
utils/graveyard: a utility that doesn't belong to any boundary is a cohesion failure waiting to be named. - Boundaries match teams when they can: a boundary that matches a team's ownership (lesson 0033) makes coordination follow the tree.
A convention nobody enforces decays within a quarter. Turn the rules into a test and the tree becomes a fitness function — the arrows fail the build instead of failing review:
# tests/test_architecture.py
FORBIDDEN = {
"checkout.domain": ("checkout.application", "checkout.infrastructure",
"checkout.api"),
"checkout.application": ("checkout.infrastructure", "checkout.api"),
"checkout.infrastructure": ("checkout.api",),
}
def test_dependency_direction() -> None:
for package, forbidden in FORBIDDEN.items():
for module in walk_modules(package):
for imported in imports_of(module):
assert not imported.startswith(forbidden), (
f"{module} imports {imported}: the arrow points outward"
)
Fifteen lines — walk_modules and imports_of are a few more, built on pkgutil and ast — and the boundary is now a fact about the build rather than a promise in a README. (Tools like import-linter do the same job from a config file; what matters is that something checks.)
| Folder | May import | Must never import |
|---|---|---|
domain/ | nothing outside itself | application, infrastructure, api, any SDK |
application/ | domain | infrastructure, api, any SDK |
infrastructure/ | domain, application, third-party SDKs | api |
api/ | application (and the domain types it passes) | infrastructure, directly |
Skill: draw the tree
The best primary key for folder structure is:
A dependency cycle appears between two folders. The cause is usually:
The utils/ folder is a smell because:
Practice on your own code
Draw your project's current folder tree and label each folder's job and allowed imports. Then redraw it by boundary. Count the files whose home changes — those files are your mis-organization, and the redraw is your target layout.
Reveal: a tree redraw
A project has models/, repos/, views/. The redraw puts models/ → domain/ (rules only), repos/ → infrastructure/ (adapters), and the views/ split between application/ (use cases) and api/ (delivery). The dependency cycle that used to exist between models and repos disappeared with the boundary.
Your win
You can draw a boundary-based tree, spot the folders that don't belong to a boundary, and make dependency direction visible in the layout itself.
Read and watch deeper
- Clean Architecture, Martin — ch. 12–14: component cohesion and coupling — the principles behind folder boundaries.
- Architecture Patterns with Python, Percival & Gregory — ch. 4: the package layout this lesson's tree comes from.
- A Philosophy of Software Design, Ousterhout — ch. 4 and 7: module depth and "different layer, different abstraction" applied to folders.
- Watch: ArjanCodes YouTube — search "project structure python".
- Next: lesson 0023 — Ports & Adapters for infrastructure.
- Reference: Glossary — fitness function, boundary.
Show your agent-teacher your folder tree and label every folder's allowed imports together.