Skip to content

Domain without Urich

Your domain layer can be completely free of Urich imports. Aggregates and events are plain Python types. The framework does not require any convention on the aggregate (no special method names). Event publishing is done in the command handler: the handler gets events from the aggregate in whatever way the domain provides and publishes them via EventBus.

No framework contract on the aggregate

The aggregate can have any shape (e.g. a dataclass with id and fields). The framework does not look at the aggregate result. The handler receives EventBus via constructor injection, loads/saves the aggregate, then publishes domain events explicitly (e.g. after repo.add() or repo.save()).

Domain events can be any type (e.g. dataclasses); they do not need to inherit from Urich's DomainEvent.

Example: domain with no Urich imports

# myapp/domain/employees.py
from dataclasses import dataclass


@dataclass
class EmployeeCreated:
    employee_id: str
    name: str
    role: str


@dataclass
class Employee:
    id: str
    name: str
    role: str

    @classmethod
    def from_db(cls, id: str, name: str, role: str) -> "Employee":
        return cls(id=id, name=name, role=role)

Module and handler

Register the aggregate and the handler. The handler is responsible for publishing domain events.

DomainModule("employees")
    .aggregate(Employee)
    .repository(IEmployeeRepository, EmployeeRepositoryImpl)
    .command(CreateEmployee, CreateEmployeeHandler)
    .on_event(EmployeeCreated, on_employee_created)

The command handler injects EventBus and publishes events after saving the aggregate:

class CreateEmployeeHandler:
    def __init__(self, employee_repository: IEmployeeRepository, event_bus: EventBus):
        self._repo = employee_repository
        self._event_bus = event_bus

    async def __call__(self, cmd: CreateEmployee) -> str:
        employee = Employee(id=cmd.employee_id, name=cmd.name, role=cmd.role)
        await self._repo.add(employee)
        await self._event_bus.publish(EmployeeCreated(employee_id=employee.id, name=employee.name, role=employee.role))
        return employee.id

The framework returns the handler result in the HTTP response (result.id here). It does not inspect the aggregate or publish events itself.