Files
docs/standards/src/lib/02_layering_rules.md

1.7 KiB

Layering Rules

Summary

The dependency direction is fixed. Violating it is a design defect.


Allowed imports

interfaces/*      ->  domain/*            (services, ports, models)
interfaces/*      ->  infra/adapters/*    (only read-only value objects)
domain/*          ->  domain/*            (same layer)
domain/*          ->  pipelines/*         (call a pipeline via a port)
infra/adapters/*  ->  domain/ports/*      (implement the interface)
main.py/init.py   ->  everything          (composition root only)

Forbidden imports

domain/*          ->  interfaces/*        (never)
domain/*          ->  fastapi/starlette   (never)
infra/adapters/*  ->  interfaces/*        (never)
interfaces/*      ->  infra/db/*          (state lives in resources, not imports)

Why

  • A pure domain is testable without a server, a database, or an event loop.
  • Adapters can be swapped (in-memory fake, real database) without touching business logic.
  • The HTTP edge stays thin and replaceable.

The port/adapter contract

A port is an abstract class or protocol declared in domain/<x>/ports/. An adapter implements it in infra/adapters/. The composition root binds the two.

domain/<resource>/ports/db.py       -> repository port + document model
domain/<resource>/ports/service.py  -> service + factory
infra/db/repos/<vendor>.py          -> repository adapter factory
infra/adapters/service.py           -> generic CRUD service base

Enforcement

  • Keep every domain module importable with only the standard library and Pydantic. If a domain import pulls in fastapi, the layering is broken.
  • Reviewers (and CI) reject import fastapi inside domain/ or infra/.