Domain module
A DomainModule is one object that describes a bounded context: aggregate, repository(ies), commands, queries and domain event handlers. Register it with app.register(orders_module).
Fluent API
from urich.ddd import DomainModule
orders_module = (
DomainModule("orders")
.aggregate(Order)
.repository(IOrderRepository, OrderRepositoryImpl)
.command(CreateOrder, CreateOrderHandler)
.query(GetOrder, GetOrderHandler)
.on_event(OrderCreated, on_order_created)
)
DomainModule(name, prefix=None)—nameis the context name;prefixdefaults to"/{name}"(e.g./orders)..aggregate(root)— Registers the aggregate root type (used by convention; repository and handlers work with it)..repository(interface, impl)— Registers the repository: interface in the container resolves to the implementation. Can be called multiple times for different repositories..command(cmd_type, handler)— One command type (dataclass) and one handler (class or callable). AddsPOST /{prefix}/commands/{snake_case(cmd_type.__name__)}..query(query_type, handler)— One query type and one handler. AddsGETandPOSTfor/{prefix}/queries/{snake_case(query_type.__name__)}..on_event(event_type, handler)— Subscribes the handler to the EventBus for this domain event. If no EventBus is registered, an in-process dispatcher is used automatically.
Project structure
| Layer | Role |
|---|---|
| domain | Aggregate root (subclass of AggregateRoot), domain events (subclass of DomainEvent). |
| application | Command/query dataclasses (subclass of Command / Query), handler classes or functions. |
| infrastructure | Repository interface (e.g. IOrderRepository) and implementation (in-memory, DB, etc.). |
| module.py | Single DomainModule(...) instance; import and pass to app.register(). |
Routes
| Kind | HTTP | Path pattern | Body (POST) / GET params |
|---|---|---|---|
| Command | POST | /{prefix}/commands/{command_name} |
JSON → command dataclass |
| Query | GET, POST | /{prefix}/queries/{query_name} |
GET: query params; POST: JSON → query dataclass |
Command/query names are derived from the dataclass name in snake_case (e.g. CreateOrder → create_order).
Handlers
Handlers can be:
- A class — Registered in the container and instantiated with constructor injection. The framework calls the instance with the command/query (handler must be callable:
__call__(self, cmd)orasync __call__(self, cmd)). - A function — Called directly with the command/query. Can be async.
Example class handler with DI:
class CreateOrderHandler:
def __init__(self, order_repository: IOrderRepository, event_bus: EventBus):
self._repo = order_repository
self._event_bus = event_bus
async def __call__(self, cmd: CreateOrder) -> str:
order = Order(id=cmd.order_id, customer_id=cmd.customer_id, total_cents=cmd.total_cents)
await self._repo.add(order)
for event in order.collect_pending_events():
await self._event_bus.publish(event)
return order.id
The container resolves IOrderRepository and EventBus and injects them into the constructor.
EventBus and event handlers
- If you registered EventBusModule (or another module that registers
EventBus), that instance is used. - If not, DomainModule registers an InProcessEventDispatcher as the EventBus automatically.
.on_event(OrderCreated, handler)subscribeshandlertoOrderCreated. Handlers are invoked when you callawait event_bus.publish(event)(e.g. from a command handler after saving the aggregate).
Response format
- Command endpoint returns JSON:
{"ok": true, "result": <handler return value>}or{"ok": true}if the handler returnsNone. - Query endpoint returns JSON: the handler’s return value directly (or
{}ifNone).
Errors in handlers are not caught by the framework; let them bubble so your ASGI server or middleware can handle them.