Skip to content
Packages Examples Agents Blog Get started

Architecture

Oridecon’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 from two packages to dozens.

graph TB
    subgraph L1["oridecon-contracts — zero dependencies"]
        P[Protocols] 
        T[Types & Value Objects]
        E[Exceptions]
    end
    subgraph L2["oridecon — depends only on contracts"]
        C[Container / DI]
        A[Application & Lifecycle]
        M[Modules & Providers]
    end
    subgraph L3["oridecon-* extensions"]
        W[oridecon-web]
        S[oridecon-sql]
        AI[oridecon-ai-*]
        More[the rest of the catalog]
    end
    L1 --> L2
    L2 --> L3
    L1 --> L3
LayerMay depend onNever depends on
oridecon-contractsnothinganything
oridecon (core)oridecon-contractsany extension
oridecon-* (extension)oridecon + oridecon-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.

  • oridecon-sql doesn’t import oridecon-cache. If a SQL feature wants caching, it depends on CacheBackendProtocol (a contract), and the container injects whatever cache implementation is registered — Redis, in-memory, or a test fake.
  • Swap without ripple. Because dependencies are expressed as protocols in oridecon-contracts, replacing one implementation never forces a change in another package.
  • Install à la carte. You can install oridecon-web without pulling in oridecon-ai-llm, and vice versa. There is no hidden web of inter-package coupling.

When two extensions genuinely need to collaborate, they do it through a shared contract in oridecon-contracts, not a direct import. Documented exceptions (admin, AI/multimedia orchestrators, testing) live on Compatibility.


The package graph above is not the application tree. oridecon 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.pycreate_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 oridecon import namespace (a PEP 420 namespace package), even though they are separate distributions:

oridecon-web/ → src/oridecon/web/ → import: from oridecon.web import ...
oridecon-sql/ → src/oridecon/sql/ → import: from oridecon.sql import ...
oridecon-ai/ → src/oridecon/ai/ → import: from oridecon.ai import ...

So installing the oridecon-web distribution gives you the oridecon.web module. One consistent import root; many independently versioned packages underneath.

from oridecon import Application, Provider # core
from oridecon.web import WebModule, get # oridecon-web distribution
from oridecon.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 oridecon-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 oridecon import Application
from oridecon.sql import DatabaseModule
from oridecon.web import WebModule
app = Application(name="my-app")
app.add_module(DatabaseModule.configure()) # reads sql: from application.yaml
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 (database, cache) is ready before the web layer starts serving. Full tree: Project Structure.


PropertyHow the boundary rule delivers it
TestabilityDepend on contracts → substitute fakes from oridecon-testing with no production code change.
ReplaceabilitySwap Redis for Memcached, Postgres for SQLite, one LLM provider for another — through config, not refactors.
Incremental adoptionStart with 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.