Software Design Reference System Designer · Managing Complexity

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 typemodels/, 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:

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.)

The foldering test: open the tree and point at each folder, saying what it is and what it may import. If you hesitate on a folder's job or its allowed imports, that folder is a boundary that hasn't been drawn.
Field notes · the import table, written down once
FolderMay importMust never import
domain/nothing outside itselfapplication, infrastructure, api, any SDK
application/domaininfrastructure, api, any SDK
infrastructure/domain, application, third-party SDKsapi
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

Show your agent-teacher your folder tree and label every folder's allowed imports together.