Skip to content

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.

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
LayerMay depend onNever depends on
quadkit-contractsnothinganything
quadkit (core)quadkit-contractsany extension
quadkit-* (extension)quadkit + quadkit-contractsanother 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-web doesn’t import quadkit-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-web and quadkit-testing are 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.


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.

KindWhere it lives
Composition rootsrc/<app>/app.py — create_app(), ASGI target <app>.app:app
Unscoped domain typessrc/<app>/domains/
App providerssrc/<app>/di/*_provider.py
Module providersrc/<app>/modules/<slug>/provider.py
Cross-cuttingsrc/<app>/shared/

If a skill or the CLI dump disagrees, this site wins. Full map: Project Structure.


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 # core
from quadkit.web import WebModule, get # quadkit-web distribution
from quadkit.contracts.core.di import BootContainerProtocol # contracts

An extension contributes to an application in three ways, all built on the core primitives:

MechanismRoleCovered in
ProviderRegisters the extension’s services in the container and manages their lifecycleProviders
ModuleBundles providers with import/export boundaries; usually exposes configure()Modules
ContractThe protocol(s) the extension implements or depends on, defined in quadkit-contractsContainer 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 Application
from 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.


PropertyHow the boundary rule delivers it
TestabilityDepend on contracts → substitute fakes from quadkit-testing with no production code change.
ReplaceabilitySwap one backend for another through config, not refactors — the package you replace is never referenced by name.
Incremental adoptionStart with the core two packages; add extensions one at a time without untangling dependencies.
Clear ownershipEach package has one purpose and a well-defined surface; large teams can own packages independently.