mills¶
Purpose: Business logic and services — framework-free.
mills is where domain rules live. It has no knowledge of HTTP, ORM models, or
any specific framework. It operates entirely on the contracts defined in pacts
and reaches data through the repository protocols defined there.
Depends on / depended on by¶
| Depends on | pacts, specs |
| Depended on by | inits — gates reach services through pacts protocols |
"Framework-free" is about side effects, not package names. An import is
forbidden in mills if it does IO, touches global state, or owns control flow
— no ORM, no HTTP machinery, no CLI parser, no settings access. Pure
computation is fine wherever it comes from: django.utils.text.slugify is a
string function that happens to live in a framework namespace. The test: could
you copy the function's body into your project and change nothing about your
design? How strictly to enforce the line is a per-project choice — see the
Import Linter guide.
The clock, an id, a log line¶
What the rule guards against is destructive operations and output the program depends on — not everything ambient. Reading the clock, drawing a random number, minting a UUID, writing a log line: none of those destroys anything, the clock sits there like the CPU sits there, and a log no correctness depends on is invisible. Each is mockable when a test needs it fixed. None of them makes a mill impure.
Logging still reads better at the edge — a mill raises, and the gate that catches decides what to record. That is a preference, not a rule.
Readability is the real test for a generated value. If a timestamp or an id is
input to what the service does rather than something it produces on the way,
take it as an argument and let the caller make it. When the making is somebody
else's job, that caller is inits handing over a link — and a link earns its
place even if the wrapper is one line, because what it names is an external
capability, not a line count.
If a service needs data access, it receives repository protocols via constructor injection.
What it contains¶
- Service classes implementing business use cases
- Business invariants, enforced in service code
- Domain logic (semantic validation, computation, orchestration)
- Nothing that touches HTTP, templates, forms, ORM models, or framework internals
GLIMPSE does not prescribe DDD tactical patterns — no aggregate or value-object
classes are expected. Data moves as DTOs and write TypedDicts from pacts;
the rules live in the services. The slicing axes are GLIMPSE's own — nouns and
verbs, not subdomains and bounded contexts — see
slicing.
Validation: mills own the meaning¶
Gates validate format, mills validate meaning. A gate checks that input
parses — an email, an int, a date. A mill checks that it makes sense — "email
or username required", "no more than MAX_SESSION_SEATS seats" (a specs
constant, which only mills may read). The line is not single-field versus
cross-field; it is parse versus semantics.
Services take the protocols they use¶
A service declares the two or three repository protocols it actually needs, plus
a TransactionProtocol if it writes. With an ambient ORM (Django), it does not
take a whole Unit of Work — that hands the service a surface far wider than its
job. (With a session-based ORM like SQLAlchemy, the session already is a unit
of work; injecting one there is idiomatic, not a violation.)
class InvoiceService:
def __init__(
self,
invoices: InvoiceRepositoryProtocol,
customers: CustomerRepositoryProtocol,
transaction: TransactionProtocol,
) -> None:
self._invoices = invoices
self._customers = customers
self._transaction = transaction
def issue(self, data: CreateInvoiceDict) -> InvoiceDTO:
with self._transaction.atomic():
...
This is the interface segregation principle applied at the service boundary. The
concrete implementations are constructed by inits — never imported from
links.
Services may call other services when they are reusing real orchestration. See Dependency direction for which calls are fine and which are smells.
Boundary vs core — what belongs here¶
Code that enforces business rules is core, and core is mills. Code that
crosses a boundary is a contract, and contracts are pacts — including
DTOs, which feel like domain objects and are not. See boundary vs
core.
Slicing axis¶
Start as a single mills.py module. Promote to a package sliced by noun,
then verb, as the layer grows. pacts uses the same axis, so the two trees
tend to look alike — but each promotes on its own schedule, when its own size
or friction says so.
mills mirrors the domain, not the interface. A verb cut names a real
activity (issue, refund, enroll); if the only name you can find is
manage or misc, the file is not too big yet.
mills.py # start here, alongside pacts.py
mills/invoices.py # promoted when mills.py stops being comfortable
mills/users.py
mills/invoices/issue.py # cut by verb when the noun grows fat
mills/invoices/refund.py
Red flags¶
The registry lives in one place: mills red flags, plus layout and slicing for the entries that cut across layers — the port axis and the catch-all verb module.