Skip to content

← Seams, Modules, Packaging and Tooling step 11 of 36

Hard Framework

The layering contract, as a test

Write the fitness function that keeps a layered architecture from decaying into a directory.

def check_layering(
    module_layers: Mapping[str, str],
    layer_order: Sequence[str],
    imports: Mapping[str, Sequence[str]],
) -> list[tuple[str, str]]:
  • module_layers maps a module name to the name of its layer.
  • layer_order lists the layers innermost first, e.g. ["domain", "application", "adapters", "entrypoints"].
  • imports maps a module to the modules it imports.

Return every (importer, imported) pair that points outward — where the imported module’s layer sits later in layer_order than the importer’s.

The rules that make it usable in a real repository:

  • Same-layer imports are legal. domain.order importing domain.money is not a finding. Only strictly-outward edges are.
  • Unknown modules are ignored, on both sides. decimal, httpx and a one-off script that is not in module_layers produce no findings. A contract that fires on every stdlib import is a contract that gets disabled.
  • A layer not present in layer_order is ignored too. That is how you park an unclassified area of the codebase without switching the check off for everything else.
  • The result is deduplicated and sorted. A module listed twice in the same import list is one violation, and the output must not depend on dict ordering — an unstable list means a diff nobody can review.

What you are actually building. This is import-linter‘s layers contract in fifteen lines. The value is not the algorithm; it is that once check_layering(...) == [] is an assertion in your suite, “the domain must not import the database” stops being a code-review opinion that a tired reviewer waves through and becomes a red build on the branch that broke it.

Note what the direction buys you: with zero outward edges, domain can be imported in a fresh interpreter with sqlite3 and httpx unavailable. That is a test you can write, and it is the real proof that the layering is more than a folder structure.

Your submission must pass mypy --strict. module_layers.get(name) returns str | None and both of those Nones are load-bearing rules, not oversights to silence.