Skip to content

Layers Overview

GLIMPSE defines seven layers: six inner layers, each with a single responsibility and a fixed set of allowed dependencies, plus edges — the framework shell outside the import rules.

Dependency diagram

┌─────────────────────────────────────────────┐
│  edges   (settings, wsgi/asgi — outside GLIMPSE)
└─────────────────────────────────────────────┘
         ↓ names inits in settings — string, not import
┌──────────────────────────────────────────┐
│                  inits                   │  DI container, middleware
└──────────────────────────────────────────┘
     ↓               ↓               ↓
┌──────────┐    ┌──────────┐    ┌──────────┐
│  gates   │    │  mills   │    │  links   │
└──────────┘    └──────────┘    └──────────┘
     │               ↓               │
     │          ┌──────────┐         │
     │          │  specs   │  invariants — only mills may import
     │          └──────────┘         │
     ↓               ↓               ↓
┌──────────────────────────────────────────┐
│                  pacts                   │  contracts — depends on nothing
└──────────────────────────────────────────┘

Arrows point in the direction of dependency (A → B means A imports B), with one exception: edges → inits is configuration, not import — settings name the middleware by dotted string, and nothing ever imports edges.

gates, mills, and links are siblings that never import each other, and pacts is the only layer any of them may reach. Everything they need from one another arrives as a protocol implemented elsewhere and passed in. inits is the single place the three meet — it holds the concrete classes, builds the object graph, and hands each side what it was promised.

inits → gates is port-dependent. inits is the composition root and the only layer that may import gates. Where the framework dispatches to entry points itself (web), inits never needs to — middleware attaches the container to the request, and the arrow is injection, not import. Where nothing dispatches (CLI), inits imports the gate classes and composes them directly.

specs sits between mills and pacts and has exactly one consumer. links, gates, and inits must never import it — a constant they need is either a contract (pacts) or configuration, which enters at inits.

Layer summary

Layer Purpose Depends on Imported by
pacts Protocols, DTOs, errors, enums, TypedDicts nothing everything
specs Business invariants (pure constants, no IO) pacts mills only
mills Business logic and services pacts + specs inits
links Repositories, external clients pacts + ORM inits
gates Views, forms, URLs, templatetags, CLI commands pacts inits (CLI composition only)
inits DI container, middleware — wires links into gates pacts + mills + links (+ gates in CLI) + framework glue nothing — edges names it in configuration
edges settings, wsgi/asgi, manage.py outside GLIMPSE nothing

Package or module?

pacts, specs, and mills are sliced by noun — and on day one you have one mills.py and no reason to plan further. (pacts also holds port and wiring contracts in their own modules — see the placement algorithm.) inits has no axis to get right — it stays thin, so split it however is convenient. All four begin as single modules (mills.py) and are promoted to packages (mills/) when they earn it.

links and gates are packages from day one. Their first axis is the port, and the port is knowable before a line of code is written: you know you are building a CLI, you know you are talking to a database. Skipping the axis means renaming every import the day a second adapter arrives.

myproject/
├── pacts.py
├── specs.py
├── mills.py
├── inits.py
├── links/
│   └── db/
│       └── sqlite.py
├── gates/
│   └── cli/
│       └── argparse.py
└── edges/
    └── __init__.py       # empty until a framework fills it

edges/ is there and empty. A CLI has nothing to put in it — the runtime reaches inits by dotted string in pyproject.toml — but the import contracts are taken as a set on the first commit, and a contract can only name a module that exists.

See Growing rules for what triggers the promotion.

Keep __init__.py empty

The default is an empty __init__.py, with every symbol imported from the module that defines it — from pkg.foo.bar import Bar, not from pkg.foo import Bar.

A facade __init__.py that re-exports a public surface is allowed only for:

  • a framework or public-API package whose inner layout is an implementation detail (the links adapter facade)
  • relief from line-length pressure
  • a pre-existing legacy facade

It is not the default.