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.
1. Cross-extension import
Section titled “1. Cross-extension import”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 = engineFix — depend on a protocol from oridecon-contracts, bind the SQL impl in a provider:
from oridecon.contracts.data import RepositoryProtocolfrom 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.
2. unwrap() without is_ok()
Section titled “2. unwrap() without is_ok()”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.
3. models/ instead of domains/
Section titled “3. models/ instead of domains/”There is no models/ directory. Feature types live in domains/. App providers live in di/.
Fail:
src/my_app/models/user.pysrc/my_app/providers/user_provider.pyFix:
src/my_app/domains/user.pysrc/my_app/di/user_provider.pyoridecon gen service users # src/my_app/services/…oridecon new module billing # src/my_app/modules/billing/domains/ + provider.pyDo not invent a second layout. Templates add packages, not a different shape.
4. register() resolves
Section titled “4. register() resolves”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()5. Service locator
Section titled “5. Service locator”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 raiseFix:
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 rowFix — 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.
8. Any, print(), relative imports
Section titled “8. Any, print(), relative imports”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 UserRepositoryProtocolfrom oridecon.logging import get_logger
log = get_logger(__name__)
class UserService: def __init__(self, repo: UserRepositoryProtocol) -> None: self.repo = repoAbsolute imports. Typed constructors. Structured logs.
9. Mocks in production src/
Section titled “9. Mocks in production src/”Fail — shipping a fake with the app:
src/my_app/services/fake_user_repo.pyFix — production src/ talks to protocols. Fakes live in tests, or uv add --dev oridecon-testing.
10. Listing controllers in app.py
Section titled “10. Listing controllers in app.py”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 appFix:
WebModule.configure(discover=["my_app.controllers", "my_app.modules"])oridecon gen controller users drops a file under controllers/. The next oridecon run finds it.
What to copy instead
Section titled “What to copy instead”| Need | Copy |
|---|---|
| SQL stays in the repository | examples/sql-repository |
| Cookie + JWT | examples/auth-web |
| Module boundary + RBAC | examples/auth-rbac |
| Agent loop with tools on the container | examples/support-agent |
Rules: /agents.md. Playbook: For coding agents. Skill: /SKILL.md.
Next Steps
Section titled “Next Steps”- For coding agents — recipes that pass CI
- Project Structure — the tree generators write
- Your First App — scaffold and run
- Examples — copy a living app