Skip to content

File Layout

pacts, specs, mills, and inits start as single modules and grow into packages. links and gates are packages from the first commit, because their first axis — the port — is known before any code exists.

Path patterns per layer

pacts.py                                 # start here
pacts/{noun}.py
pacts/{noun}/{verb}.py
pacts/{port}.py                          # port machinery, e.g. pacts/db.py
pacts/services.py                        # ServicesProtocol + service protocols

specs.py
specs/{noun}.py

mills.py
mills/{noun}.py
mills/{noun}/{verb}.py

inits.py
inits/repositories.py                    # inits stays thin — split it as convenient
inits/services.py

links/{port}/{adapter}.py                # e.g. links/db/sqlite.py
links/{port}/{adapter}/{kind}.py         # e.g. links/db/postgres/repositories.py
links/{port}/{adapter}/{kind}/{module}.py
links/{port}/{adapter}/__init__.py       # facade — the public surface

gates/{port}/{adapter}.py                # e.g. gates/cli/argparse.py
gates/{port}/{adapter}/{page}.py
gates/{port}/{adapter}/{page_group}/{page}.py   # whichever grouping the
gates/{port}/{adapter}/{page}/{subpage}.py      # interface already has

Splitting rules

Split a file when it reaches ~1000 lines, or earlier when two unrelated concerns cause merge friction. Never create a nested directory before files exist to fill it — a folder needs at least two leaves to justify existing.

See Growing rules for the full set of thresholds.

Correct progression for a growing invoices noun:

# Start flat — one module per layer
pacts.py
mills.py

# Promote when a second noun appears, or the module grows large
pacts/
├── __init__.py
├── invoices.py
└── users.py

mills/
├── __init__.py
├── invoices.py
└── users.py

# Cut by verb when the noun's activities diverge
pacts/invoices/
├── __init__.py
├── issue.py
└── refund.py

mills/invoices/
├── __init__.py
├── issue.py
└── refund.py

pacts and mills use the same axis, so their trees tend to look alike — but each promotes when its own size or friction says so, not in lockstep.

Naming conventions

Axis Naming style Examples
Noun lowercase, no separators invoices, users, events, panel
Verb lowercase, no separators issue, refund, enroll
Page lowercase, follows the interface dashboard, checkout, export
Port lowercase, snake_case cli, web, db, payment_api
Adapter lowercase argparse, postgres, stripe
Kind lowercase, plural models, repositories

Nouns are not forced to a single plurality — events are many, a panel is one. Name each after the thing it is.

Project root layout

A small project:

myproject/
├── pacts.py
├── specs.py
├── mills.py
├── inits.py
├── links/
│   └── db/
│       └── sqlite.py
├── gates/
│   └── cli/
│       └── argparse.py
└── edges/
    └── __init__.py

All seven layers exist from the first commit, edges/ empty because a CLI has nothing to put in it — pyproject.toml names the inits entry point by dotted string. The empty package is what lets the edges import contract run from day one. It fills when a framework does: settings, wsgi.py, manage.py.

The same project grown:

myproject/
├── pacts/
├── specs/
├── mills/
├── links/
│   ├── db/
│   │   └── postgres/
│   │       ├── __init__.py       # facade
│   │       ├── models.py
│   │       └── repositories.py
│   └── payment_api/
│       └── stripe.py
├── gates/
│   ├── web/
│   │   └── flask/
│   └── cli/
│       └── argparse/
├── inits/
│   ├── repositories.py
│   └── services.py
└── edges/
    ├── settings/
    └── wsgi.py

What lands in edges/ is whatever the framework owns: settings plus a WSGI entry point here, and a manage.py too on Django. See edges.

__init__.py policy

Keep __init__.py empty by default, and import each symbol from the module that defines it:

from myproject.pacts.invoices import InvoiceDTO   # correct
from myproject.pacts import InvoiceDTO            # avoid

The sanctioned exceptions — the links adapter facade among them — are listed in the layers overview.

What to avoid

Every layout mistake is catalogued in Drift red flags — start with layout and slicing.