Patterns & Red Flags¶
Status: Experimental — evolving with active use
These patterns describe how GLIMPSE layers collaborate at runtime. They are conventions enforced by code review, not by importlinter.
Patterns¶
1. Views return DTOs, never models¶
Templates and API responses receive Pydantic DTOs from pacts. ORM instances never leave links.
# gates/web/django/proposals.py
def detail(request: RootRequestProtocol, pk: int) -> HttpResponse:
proposal: ProposalDTO = request.di.uow.proposals.get(pk)
return render(request, "detail.html", {"proposal": proposal})
2. Data access through UoW¶
Gates never import repositories or ORM models. All data access goes through request.di.uow.{repo}.
# correct
proposals = request.di.uow.proposals.list_active()
# wrong — imports a concrete class from links
from links.db.django.proposal import ProposalRepository
3. Repository identity map¶
Each repository follows: check cache → query ORM → store in cache → return DTO. An entity is loaded at most once per request.
4. Services take UoW via constructor¶
Mills services receive their dependencies at construction time. Never via method arguments, never via direct import.
5. Mills are framework-free¶
mills must not import from Django, SQLAlchemy, or any ORM. If a test for a mill requires a live database, the mill has leaked infrastructure.
6. Writes use TypedDicts¶
DTOs (Pydantic) are for reads. TypedDicts are for writes — they travel from gates into mills as typed input.
# pacts/proposals.py
class CreateProposalDict(TypedDict):
title: str
author_id: int
class ProposalDTO(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
title: str
7. Request typed as RootRequestProtocol¶
Gate functions type the request parameter as RootRequestProtocol from pacts, not as the framework's concrete request class.
8. Multi-repo writes use uow.atomic()¶
Any operation that writes to more than one repository is wrapped in uow.atomic() to ensure atomicity.
9. New repo methods need a matching Protocol in pacts¶
Before adding a method to a repository in links, define it in the corresponding Protocol in pacts. mills depends on the protocol, not the concrete class.
10. New DTOs need from_attributes=True¶
Every new Pydantic DTO in pacts needs:
This allows repositories to construct DTOs from ORM instances via ProposalDTO.model_validate(orm_instance).
11. New cached entities go on Storage; repos as @cached_property on UoW¶
When adding a new entity:
- Add it to
Storageinlinksif it needs in-memory caching - Expose its repository as a
@cached_propertyon the UoW class
Drift red flags¶
These patterns indicate architectural drift
If you see any of these in a codebase, treat them as bugs.
- Layer kept as single file instead of package
pacts.py,mills.py, etc. at the project root. Each layer must be a directory. A single file cannot be split without breaking imports.- Nested folders holding one or two small files
pacts/billing/invoicing/create.pywhenpacts/billing/invoicing.pywould do. Premature nesting makes the structure harder to navigate without adding clarity.- Port axis inside mills or specs
mills/web/proposals.pyorspecs/api/.... Mills and specs have no delivery-mechanism axis. If you see a port word inside these layers, the code belongs elsewhere.- pacts split by technical kind instead of subdomain
pacts/dtos.py,pacts/protocols.py,pacts/repos/. These group by what the type is, not by what domain concern it belongs to. This forces unrelated subdomains to share files and makes the package harder to navigate.- common/ or shared/ folder in any layer
- This is a magnet for unrelated code. Extract truly shared types to
pacts; if something is shared across layers, it belongs there. - Mismatched slicing axes between pacts and mills
pacts/billing/invoicing.pyexists butmills/billing.pyhas not split yet — or vice versa. The two layers must mirror each other.- links/db/django/{context}.py
linksfiles are per-entity, not per-subdomain or context.links/db/django/billing.pyholding multiple entities' models is a sign that it needs to be split by entity.- mills/{entity}.py holding context-specific write logic
- Entity-level mills (e.g.
mills/proposal.py) are only for entity-level invariants that don't belong to any specific bounded context. Context-specific logic belongs in a context-level mill file.