Skip to content
GitHubDiscord

Project Structure

For applications with a handful of domain services, explicit providers, and YAML config.

my-app/
β”œβ”€β”€ application.yaml # Configuration
β”œβ”€β”€ pyproject.toml
└── src/
└── my_app/
β”œβ”€β”€ __init__.py
β”œβ”€β”€ app.py # create_app() β€” composition root
β”‚
β”œβ”€β”€ providers/
β”‚ └── app_provider.py # Domain service registration
β”‚
β”œβ”€β”€ domain/
β”‚ β”œβ”€β”€ models.py # Entity / ValueObject classes
β”‚ └── services.py # Business logic
β”‚
└── api/
└── controllers/
└── user_controller.py
FilePurpose
app.pyThe composition root β€” create_app() factory that wires providers. Auto-detected by lexigram run
application.yamlConfiguration β€” loaded by LexigramConfig.from_yaml()
providers/Provider subclasses that bind services in the DI container via register()
domain/Business logic β€” framework-agnostic models and services
api/controllers/Controller subclasses auto-discovered by WebProvider.auto_discover()
src/my_app/app.py
from lexigram import Application, LexigramConfig
from lexigram.web import WebProvider
from my_app.providers.app_provider import AppProvider
def create_app() -> Application:
config = LexigramConfig.from_yaml()
app = Application(name="my-app", config=config)
app.add_provider(AppProvider())
app.add_provider(WebProvider.auto_discover("my_app.api.controllers"))
return app
src/my_app/providers/app_provider.py
from lexigram.di.provider import Provider
from lexigram.contracts.core import ProviderPriority
from lexigram.contracts.core.di import ContainerRegistrarProtocol
class AppProvider(Provider):
name = "app"
priority = ProviderPriority.DOMAIN
async def register(self, container: ContainerRegistrarProtocol) -> None:
from my_app.domain.services import UserService
container.singleton(UserService, UserService)
src/my_app/api/controllers/user_controller.py
from lexigram.web import Controller, get, post
from lexigram.result import Result
from my_app.domain.services import UserService
from my_app.domain.models import User
class UserController(Controller):
prefix = "/api/v1/users"
def __init__(self, user_service: UserService) -> None:
self.user_service = user_service # injected by the container
@get("/")
async def list_users(self) -> list[User]:
return await self.user_service.list_all()
@get("/{user_id}")
async def get_user(self, user_id: str) -> Result[User, str]:
return await self.user_service.find(user_id)

For multi-team applications with module boundaries, import/export visibility, and environment profiles.

my-platform/
β”œβ”€β”€ application.yaml # Base config
β”œβ”€β”€ application.production.yaml # Profile override
β”œβ”€β”€ application.staging.yaml
β”œβ”€β”€ pyproject.toml
└── src/
└── my_platform/
β”œβ”€β”€ __init__.py
β”œβ”€β”€ app.py # create_app() β€” composition root
β”‚
β”œβ”€β”€ infrastructure/
β”‚ β”œβ”€β”€ __init__.py # @module InfraModule
β”‚ β”œβ”€β”€ db_provider.py
β”‚ └── cache_provider.py
β”‚
└── modules/
β”œβ”€β”€ auth/
β”‚ β”œβ”€β”€ __init__.py # @module AuthModule (boundary)
β”‚ β”œβ”€β”€ protocols.py # AuthServiceProtocol (the contract)
β”‚ β”œβ”€β”€ provider.py # AuthProvider
β”‚ β”œβ”€β”€ services.py # AuthService (implementation)
β”‚ └── controllers/
β”‚ └── auth_controller.py
β”‚
└── billing/
β”œβ”€β”€ __init__.py # @module BillingModule
β”œβ”€β”€ protocols.py
β”œβ”€β”€ provider.py
└── services.py
ConventionWhy
__init__.py is the module boundaryContains the @module class β€” the public API of the package
protocols.py defines the contractOther modules import protocols, never concrete implementations
exports=[ ] controls visibilityOnly exported types are accessible to importing modules
providers/ stays internalProviders register services but are never imported by other modules
src/my_platform/modules/auth/__init__.py
from lexigram.di.module import module
from my_platform.modules.auth.provider import AuthProvider
from my_platform.modules.auth.protocols import AuthServiceProtocol
@module(
providers=[AuthProvider],
exports=[AuthServiceProtocol],
)
class AuthModule:
"""Authentication β€” only AuthServiceProtocol is visible to importers."""
src/my_platform/app.py
from lexigram import Application, LexigramConfig, CoreModule
from lexigram.web import WebModule
from my_platform.infrastructure import InfraModule
from my_platform.modules.auth import AuthModule
from my_platform.modules.billing import BillingModule
def create_app(profile: str | None = None) -> Application:
config = LexigramConfig.from_env_profile(profile)
app = Application(name="my-platform", config=config)
app.add_module(CoreModule)
app.add_module(InfraModule)
app.add_module(AuthModule)
app.add_module(BillingModule)
app.add_module(WebModule.auto_discover("my_platform.modules"))
return app

You can also mix standalone providers with modules in the same app:

app.add_module(AuthModule) # module with boundaries
app.add_provider(MetricsProvider()) # standalone β€” globally visible

Instead of manually importing every provider, use discover_providers() to scan packages:

app = Application(name="my-app", config=config)
app.discover_providers("my_app.modules", "my_app.infrastructure")

This scans each package recursively for Provider subclasses and @injectable / @singleton classes, then auto-registers them.


  • Core Concepts β€” Providers, DI, Result type, and modules in depth
  • Configuration β€” YAML config, profiles, and auto-injection