Комментарии 6
Может, лучше не выдумывать?
Axum
async fn create_employee(
State(handler): State<Arc<CreateEmployeeHandler>>, // DI
Json(cmd): Json<CreateEmployee>, // Десериализация
) {
handler.handle(cmd).await;
}
Router::new()
.route("/employees/.../create", post(create_employee))
.with_state(handler_arc);Я хотел, чтобы маршруты и методы не задавались вручную, а выводились из доменного слоя: описал команды и запросы в DDD-терминах — и конвенция сама даёт пути и HTTP-методы.
Возможно, в статье это не получилось донести — спасибо, что написали, буду иметь в виду.
Доменный слой диктует инфраструктрный? Не получиться ли потом что придётся подстраиваясь под внешние требования менять домен. Почему вы думаете что всегда будете диктовать контракт. Опять же что делать если нужно сменить протокол, транспорт, сделать асинхронный контракт на очередях? В том то и прелесть отделения инфраструктуры от домена, что можно по разному доменный слой использовать.
У нас как раз не так: домен не диктует инфраструктуру. Домен задаёт только порты (интерфейсы): например, Repository с методами get/add/save и протокол EventBus. Никаких требований к БД, протоколу или транспорту в домене нет.
Смена протокола, транспорта, асинхронность: домен и use case’ы от этого не зависят. HTTP в коде — это дефолтный адаптер в фреймворке (удобно «из коробки»), а не диктат домена. Те же команды/хендлеры можно вызывать из другого модуля через gRPC, очереди (AMQP/Kafka) и т.п. — поменяется только слой адаптеров на границе приложения, домен и прикладная логика остаются теми же.
Пример из кода (bounded context orders)
Домен — только сущности и события, без импортов из инфраструктуры:
# domain.py
from dataclasses import dataclass
from urich.domain import DomainEvent
@dataclass
class Order:
id: str
customer_id: str
total_cents: int
@dataclass
class OrderCreated(DomainEvent):
order_id: str
...
Приложение зависит только от интерфейса репозитория, не от реализации:
# application.py
from .infrastructure import IOrderRepository # интерфейс
class CreateOrderHandler:
def __init__(self, order_repository: IOrderRepository, event_bus: EventBus):
self._repo = order_repository
Инфраструктура — контракт и реализация; сейчас in-memory, завтра может быть PostgreSQL:
# infrastructure.py
class IOrderRepository(Repository[Order]):
pass
class OrderRepositoryImpl(IOrderRepository): # можно заменить на PostgresOrderRepository
def __init__(self):
self._store: dict[str, Order] = {} # или self._session: AsyncSession
async def get(self, id: str): ...
async def add(self, aggregate: Order): ...
Модуль только связывает интерфейс с выбранной реализацией:
# module.py
orders_module = (
DomainModule("orders")
.repository(IOrderRepository, OrderRepositoryImpl) # здесь выбираем реализацию
.command(CreateOrder, CreateOrderHandler)
...
)
Чтобы перейти на другую БД или очередь — меняем только реализацию в инфраструктуре и одну строку в модуле (например, OrderRepositoryImpl → PostgresOrderRepository). Домен и приложение не трогаем.

Один endpoint — одна строка. Как мы до этого докатились на Rust