- Docs
- Core
- Fundamentals
- Architecture
Architecture
QuadKit’s most important design decision isn’t a feature — it’s a boundary rule enforced across every package. Understanding it explains why the framework stays coherent as it grows.
1. Three Layers, One Direction
Section titled “1. Three Layers, One Direction”graph TB
subgraph L1["quadkit-contracts — zero dependencies"]
P[Protocols]
T[Types & Value Objects]
E[Exceptions]
end
subgraph L2["quadkit — depends only on contracts"]
C[Container / DI]
A[Application & Lifecycle]
M[Modules & Providers]
end
subgraph L3["quadkit-* extensions"]
W[quadkit-web]
T[quadkit-testing]
C[quadkit-cli]
end
L1 --> L2
L2 --> L3
L1 --> L3
| Layer | May depend on | Never depends on |
|---|---|---|
quadkit-contracts | nothing | anything |
quadkit (core) | quadkit-contracts | any extension |
quadkit-* (extension) | quadkit + quadkit-contracts | another extension |
The dependency arrows only point downward. Contracts never import implementations; core never imports an extension; and — the rule that does the most work — extensions never import each other.
2. Why “Extensions Never Import Each Other”
Section titled “2. Why “Extensions Never Import Each Other””This single constraint is what makes packages genuinely pluggable.
quadkit-webdoesn’t importquadkit-cli. A web feature that needs project metadata depends on a contract, and the container injects whatever implementation is registered — the real one in production, a fake under test.- Swap without ripple. Because dependencies are expressed as protocols in
quadkit-contracts, replacing one implementation never forces a change in another package. - Install à la carte.
quadkit-webandquadkit-testingare independent distributions; neither pulls the other in. There is no hidden web of inter-package coupling.
When two extensions genuinely need to collaborate, they do it through a shared contract in quadkit-contracts, not a direct import. The documented exceptions (CLI and testing, which are tooling rather than application extensions) live on Compatibility.
3. The application tree (one layout)
Section titled “3. The application tree (one layout)”The package graph above is not the application tree. quadkit new project writes one shape. Templates add packages, not a second layout. There is no --structure flag and no models/ directory.
| Kind | Where it lives |
|---|---|
| Composition root | src/<app>/app.py — create_app(), ASGI target <app>.app:app |
| Unscoped domain types | src/<app>/domains/ |
| App providers | src/<app>/di/*_provider.py |
| Module provider | src/<app>/modules/<slug>/provider.py |
| Cross-cutting | src/<app>/shared/ |
If a skill or the CLI dump disagrees, this site wins. Full map: Project Structure.
4. The Namespace Package Layout
Section titled “4. The Namespace Package Layout”All packages publish into the shared quadkit import namespace (a PEP 420 namespace package), even though they are separate distributions:
quadkit-web/ → src/quadkit/web/ → import: from quadkit.web import ...quadkit-testing/ → src/quadkit/testing/ → import: from quadkit.testing import ...quadkit-cli/ → src/quadkit/cli/ → import: from quadkit.cli import ...So installing the quadkit-web distribution gives you the quadkit.web module. One consistent import root; many independently versioned packages underneath.
from quadkit import Application, Provider # corefrom quadkit.web import WebModule, get # quadkit-web distributionfrom quadkit.contracts.core.di import BootContainerProtocol # contracts5. How Extensions Plug In
Section titled “5. How Extensions Plug In”An extension contributes to an application in three ways, all built on the core primitives:
| Mechanism | Role | Covered in |
|---|---|---|
| Provider | Registers the extension’s services in the container and manages their lifecycle | Providers |
| Module | Bundles providers with import/export boundaries; usually exposes configure() | Modules |
| Contract | The protocol(s) the extension implements or depends on, defined in quadkit-contracts | Container Protocols |
Most extensions ship a configure() classmethod on their module so you add them in one line. The scaffold uses modules — create_app() in src/<app>/app.py:
from quadkit import Applicationfrom quadkit.web import WebModule
app = Application(name="my-app")app.add_module(WebModule.configure(discover=["my_app.controllers", "my_app.modules"]))WebModule.configure() constructs a WebProvider internally. You rarely add that provider by hand.
Boot order follows provider priority, so infrastructure providers are ready before the web layer starts serving. Full tree: Project Structure.
6. What This Buys You
Section titled “6. What This Buys You”| Property | How the boundary rule delivers it |
|---|---|
| Testability | Depend on contracts → substitute fakes from quadkit-testing with no production code change. |
| Replaceability | Swap one backend for another through config, not refactors — the package you replace is never referenced by name. |
| Incremental adoption | Start with the core two packages; add extensions one at a time without untangling dependencies. |
| Clear ownership | Each package has one purpose and a well-defined surface; large teams can own packages independently. |
Next Steps
Section titled “Next Steps”- Core Concepts — providers, DI, modules, and the Result type in one place
- Project Structure — the tree generators write
- The Ecosystem — the published packages and what each one does
- Container Protocols — the type-safe contracts at the heart of the boundary
- Compatibility — extras, drivers, documented exceptions