Skip to content
Packages Examples Agents Blog Get started

Common mistakes

These are not style nits. Each fail type-checks in isolation and still fails CI, or lays down a tree the CLI will not grow.

Extensions talk through contracts and the container. oridecon-web must not import oridecon-sql.

Fail — the SQL type leaked into the web package:

# inside oridecon-web (or your app pretending to be a package)
from oridecon.sql import SqlEngine # noqa: this is the bug
class UserController(Controller):
def __init__(self, engine: SqlEngine) -> None:
self.engine = engine

Fix — depend on a protocol from oridecon-contracts, bind the SQL impl in a provider:

from oridecon.contracts.data import RepositoryProtocol
from oridecon.web import Controller, get
class UserController(Controller):
def __init__(self, users: RepositoryProtocol) -> None:
self.users = users
@get("/{user_id}")
async def get_user(self, user_id: str) -> dict:
result = await self.users.find(user_id)
if result.is_ok():
return result.unwrap()
return {"error": str(result.unwrap_err())}

If two packages need the same type, it lives in oridecon-contracts. The import linter is the gate, not the type checker.

Fail:

user = (await self.repo.find(user_id)).unwrap()

Fix:

result = await self.repo.find(user_id)
if result.is_ok():
user = result.unwrap()
else:
return Err(result.unwrap_err())

unwrap() on Err raises. The whole point of Result is that the caller branches.

There is no models/ directory. Feature types live in domains/. App providers live in di/.

Fail:

src/my_app/models/user.py
src/my_app/providers/user_provider.py

Fix:

src/my_app/domains/user.py
src/my_app/di/user_provider.py
Terminal window
oridecon gen service users # src/my_app/services/…
oridecon new module billing # src/my_app/modules/billing/domains/ + provider.py

Do not invent a second layout. Templates add packages, not a different shape.

register() receives ContainerRegistrarProtocol. boot() receives ContainerResolverProtocol. Mixing them is a type error by design.

Fail:

async def register(self, container: ContainerRegistrarProtocol) -> None:
container.singleton(CacheBackend, RedisCacheBackend)
cache = await container.resolve(CacheBackend) # not on this protocol
await cache.connect()

Fix:

async def register(self, container: ContainerRegistrarProtocol) -> None:
container.singleton(CacheBackend, RedisCacheBackend)
async def boot(self, container: ContainerResolverProtocol) -> None:
cache = await container.resolve(CacheBackend)
await cache.connect()

Fail — passing the container into a service:

class OrderService:
def __init__(self, container: Container) -> None:
self.container = container
async def place(self, sku: str) -> Result[Order, DomainError]:
repo = await self.container.resolve(OrderRepositoryProtocol)
return await repo.add(sku)

Fix — constructor injection of the protocol:

class OrderService:
def __init__(self, repo: OrderRepositoryProtocol) -> None:
self.repo = repo
async def place(self, sku: str) -> Result[Order, DomainError]:
return await self.repo.add(sku)

The container wires OrderService. The service never sees the container.

6. Result from a constructor or a dead database

Section titled “6. Result from a constructor or a dead database”

Fail:

def __init__(self, url: str) -> Result[Engine, ConfigError]: # constructors return None
...
async def find(self, user_id: str) -> Result[User, DomainError]:
try:
return Ok(await self.engine.fetch(user_id))
except OperationalError as exc:
return Err(DatabaseDown(str(exc))) # infrastructure — let it raise

Fix:

def __init__(self, repo: UserRepositoryProtocol) -> None:
self.repo = repo
async def find(self, user_id: str) -> Result[User, DomainError]:
user = await self.repo.get(user_id) # OperationalError propagates
if not user:
return Err(UserNotFound(user_id))
return Ok(user)

User not found is a value. The database dying is an exception.

7. FastAPI Depends() as the composition root

Section titled “7. FastAPI Depends() as the composition root”

Starlette routing and Pydantic stay. Wiring does not live on the handler.

Fail — resolving the database on the route:

@app.get("/users/{user_id}")
async def get_user(user_id: str, db: Session = Depends(get_db)):
row = db.get(user_id)
return row

Fix — inject the protocol on the controller; bind the impl in a provider:

class UserController(Controller):
def __init__(self, users: UserRepositoryProtocol) -> None:
self.users = users
@get("/{user_id}")
async def get_user(self, user_id: str) -> dict:
result = await self.users.find(user_id)
if result.is_ok():
return result.unwrap()
return {"error": str(result.unwrap_err())}

Keep the FastAPI shapes you already have. Add a composition root. Map: Migrating from FastAPI.

Fail:

from .repo import UserRepo
class UserService:
def __init__(self, repo: Any) -> None:
self.repo = repo
print("ready")

Fix:

from __future__ import annotations
from my_app.domains.user import UserRepositoryProtocol
from oridecon.logging import get_logger
log = get_logger(__name__)
class UserService:
def __init__(self, repo: UserRepositoryProtocol) -> None:
self.repo = repo

Absolute imports. Typed constructors. Structured logs.

Fail — shipping a fake with the app:

src/my_app/services/fake_user_repo.py

Fix — production src/ talks to protocols. Fakes live in tests, or uv add --dev oridecon-testing.

Controllers are discovered. Adding a module lists it in create_app() next to WebModule — do not list controllers there.

Fail:

def create_app() -> Application:
app = Application()
app.add_controller(UserController)
return app

Fix:

WebModule.configure(discover=["my_app.controllers", "my_app.modules"])

oridecon gen controller users drops a file under controllers/. The next oridecon run finds it.

NeedCopy
SQL stays in the repositoryexamples/sql-repository
Cookie + JWTexamples/auth-web
Module boundary + RBACexamples/auth-rbac
Agent loop with tools on the containerexamples/support-agent

Rules: /agents.md. Playbook: For coding agents. Skill: /SKILL.md.