Skip to content

Layering Rules

Summary

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


Allowed imports

1
2
3
4
5
6
7
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)
domain/*/ports/*  ->  infra/*             (bind adapters + libraries at the seam)
infra/adapters/*  ->  domain/ports/*      (implement the interface)
main.py/init.py   ->  everything          (composition root only)

Forbidden imports

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

Why

  • The business logic is testable without a server 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

The ports/ package is the seam where the domain meets infrastructure. Each domain/<resource>/ports/ module declares the resource's models/document shape and exposes factory functions that bind a concrete adapter — so a port module is allowed to import from infra/ and from the persistence libraries. Everything outside ports/ in domain/ stays framework- and driver-free.

1
2
3
4
domain/<resource>/ports/db.py       -> document model + create_<resource>_repo()
domain/<resource>/ports/service.py  -> service + get_<resource>_service()
infra/db/repos/<vendor>.py          -> repository adapter factory
infra/adapters/service.py           -> generic CRUD service base

The composition root calls the port factories; nothing else constructs an adapter.

Enforcement

  • domain/ must never import the web framework (fastapi / starlette) or interfaces/. Only the ports/ seam, infra/, and the composition root touch drivers.
  • Reviewers (and CI) reject import fastapi inside domain/ or infra/.