docs(standards): align layering docs with the port seam; split runtime vs wiring diagrams

This commit is contained in:
2026-10-12 13:15:35 +05:30
parent 8a10c4768c
commit 7d1694148a
25 changed files with 230 additions and 159 deletions

View File

@@ -662,12 +662,14 @@
<span class="normal"><a href="#__codelineno-0-3">3</a></span>
<span class="normal"><a href="#__codelineno-0-4">4</a></span>
<span class="normal"><a href="#__codelineno-0-5">5</a></span>
<span class="normal"><a href="#__codelineno-0-6">6</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-0-1"><a id="__codelineno-0-1" name="__codelineno-0-1"></a>interfaces/* -&gt; domain/* (services, ports, models)
<span class="normal"><a href="#__codelineno-0-6">6</a></span>
<span class="normal"><a href="#__codelineno-0-7">7</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-0-1"><a id="__codelineno-0-1" name="__codelineno-0-1"></a>interfaces/* -&gt; domain/* (services, ports, models)
</span><span id="__span-0-2"><a id="__codelineno-0-2" name="__codelineno-0-2"></a>interfaces/* -&gt; infra/adapters/* (only read-only value objects)
</span><span id="__span-0-3"><a id="__codelineno-0-3" name="__codelineno-0-3"></a>domain/* -&gt; domain/* (same layer)
</span><span id="__span-0-4"><a id="__codelineno-0-4" name="__codelineno-0-4"></a>domain/* -&gt; pipelines/* (call a pipeline via a port)
</span><span id="__span-0-5"><a id="__codelineno-0-5" name="__codelineno-0-5"></a>infra/adapters/* -&gt; domain/ports/* (implement the interface)
</span><span id="__span-0-6"><a id="__codelineno-0-6" name="__codelineno-0-6"></a>main.py/init.py -&gt; everything (composition root only)
</span><span id="__span-0-5"><a id="__codelineno-0-5" name="__codelineno-0-5"></a>domain/*/ports/* -&gt; infra/* (bind adapters + libraries at the seam)
</span><span id="__span-0-6"><a id="__codelineno-0-6" name="__codelineno-0-6"></a>infra/adapters/* -&gt; domain/ports/* (implement the interface)
</span><span id="__span-0-7"><a id="__codelineno-0-7" name="__codelineno-0-7"></a>main.py/init.py -&gt; everything (composition root only)
</span></code></pre></div></td></tr></table></div>
<h2 id="forbidden-imports">Forbidden imports</h2>
<div class="language-text highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-1-1">1</a></span>
@@ -680,27 +682,32 @@
</span></code></pre></div></td></tr></table></div>
<h2 id="why">Why</h2>
<ul>
<li>A pure domain is testable without a server, a database, or an event loop.</li>
<li>The business logic is testable without a server or an event loop.</li>
<li>Adapters can be swapped (in-memory fake, real database) without touching
business logic.</li>
<li>The HTTP edge stays thin and replaceable.</li>
</ul>
<h2 id="the-portadapter-contract">The port/adapter contract</h2>
<p>A <strong>port</strong> is an abstract class or protocol declared in <code>domain/&lt;x&gt;/ports/</code>.
An <strong>adapter</strong> implements it in <code>infra/adapters/</code>. The composition root binds
the two.</p>
<p>The <code>ports/</code> package is the <strong>seam</strong> where the domain meets infrastructure.
Each <code>domain/&lt;resource&gt;/ports/</code> module declares the resource's models/document
shape and exposes <strong>factory functions</strong> that bind a concrete adapter — so a port
module <em>is</em> allowed to import from <code>infra/</code> and from the persistence libraries.
Everything outside <code>ports/</code> in <code>domain/</code> stays framework- and driver-free.</p>
<div class="language-text highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-2-1">1</a></span>
<span class="normal"><a href="#__codelineno-2-2">2</a></span>
<span class="normal"><a href="#__codelineno-2-3">3</a></span>
<span class="normal"><a href="#__codelineno-2-4">4</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-2-1"><a id="__codelineno-2-1" name="__codelineno-2-1"></a>domain/&lt;resource&gt;/ports/db.py -&gt; repository port + document model
</span><span id="__span-2-2"><a id="__codelineno-2-2" name="__codelineno-2-2"></a>domain/&lt;resource&gt;/ports/service.py -&gt; service + factory
<span class="normal"><a href="#__codelineno-2-4">4</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-2-1"><a id="__codelineno-2-1" name="__codelineno-2-1"></a>domain/&lt;resource&gt;/ports/db.py -&gt; document model + create_&lt;resource&gt;_repo()
</span><span id="__span-2-2"><a id="__codelineno-2-2" name="__codelineno-2-2"></a>domain/&lt;resource&gt;/ports/service.py -&gt; service + get_&lt;resource&gt;_service()
</span><span id="__span-2-3"><a id="__codelineno-2-3" name="__codelineno-2-3"></a>infra/db/repos/&lt;vendor&gt;.py -&gt; repository adapter factory
</span><span id="__span-2-4"><a id="__codelineno-2-4" name="__codelineno-2-4"></a>infra/adapters/service.py -&gt; generic CRUD service base
</span></code></pre></div></td></tr></table></div>
<p>The composition root calls the port factories; nothing else constructs an
adapter.</p>
<h2 id="enforcement">Enforcement</h2>
<ul>
<li>Keep every domain module importable with only the standard library and
Pydantic. If a domain import pulls in <code>fastapi</code>, the layering is broken.</li>
<li><code>domain/</code> must never import the web framework (<code>fastapi</code> / <code>starlette</code>) or
<code>interfaces/</code>. Only the <code>ports/</code> seam, <code>infra/</code>, and the composition root
touch drivers.</li>
<li>Reviewers (and CI) reject <code>import fastapi</code> inside <code>domain/</code> or <code>infra/</code>.</li>
</ul>