Releases: c-t-n/domino
Release list
domino 0.2.0
Async all the way down, and domain events that leave a transaction on purpose.
Highlights
AsyncRepository[T] and AsyncUnitOfWork — the async twins of the core
ports. The SQLAlchemy Async* classes now implement them instead of standing on
their own, so an async stack types the same way as a sync one.
An explicit event queue. Give a unit of work an event_bus and queue what
should leave the transaction:
uow = UnitOfWork({"orders": orders}, event_bus=bus)
with uow:
order.ship()
uow.orders.save(order)
uow.enqueue_events(*order.pull_pending_events())
# commit runs on exit, then the queued events go to the busEvents are published once the commit succeeds, dropped on rollback, and the
queue is cleared when the scope exits. You decide what is published; a use case
never holds a reference to the bus.
A use case gets its unit of work from the base constructor, exposed as
self._uow, and reaches its repositories through it. The transaction scope can
be opened by the use case or by its caller.
One unit of work per request in FastAPI. install_domino takes a factory,
called once per request, so no two requests share a session, a repository set or
an event queue.
install_domino(
app,
unit_of_work=lambda: AsyncSqlAlchemyUnitOfWork(
session_factory, {"orders": OrderRepository}, event_bus=bus
),
)The FastAPI integration no longer imports SQLAlchemy, so the two extras are
independent.
Breaking changes
install_domino(unit_of_work=...)expects a callable, not an instance —
wrap the previous argument in alambda:. Passing an instance raises a
TypeErrorexplaining the fix.- The async SQLAlchemy unit of work no longer scans the session's identity map
for aggregates with pending events. Queue them withenqueue_events(...)or
nothing is published.
Fixes
AsyncSqlAlchemyUnitOfWork.rollbackcalled an unset hook, so a rollback never
reached the session.AsyncUnitOfWork.__getattr__readself.__dictand raised on private names.- The event queue survived its scope, replaying an earlier scope's events on
every later commit. AggregateRoot._touch()guarded on the value ofupdated_at: it skipped a
falsy timestamp and raised on an aggregate declaring no such field. It now
guards on the field's presence and is a no-op without it.
Each of these is covered by a regression test — the suite goes from 158 to 188.
Full changelog: v0.1.0...v0.2.0
v0.1.0
First public release of domino — a small, dependency-free library for
tactical Domain-Driven Design in Python 3.12+.
Highlights
Tactical building blocks — subclass a base and declare fields; the right
dataclass is applied for you (PEP 681 @dataclass_transform, no @dataclass to
repeat), with full static typing preserved:
ValueObject,Entity,AggregateRoot,DomainIdDomainEvent+EventBus/EventHandler(in-memory pub/sub, handler errors
isolated)Repository[T],UnitOfWork,DomainServiceCommand+UseCase[C, R]/AsyncUseCase[C, R]Resultmonad and aDomainErrorhierarchy (validation / state / not-found)
Cross-cutting, zero plumbing
- Ambient correlation ids via
contextvars— one id per use-case call,
captured by every domain event and log line; reused across nested calls and
upstream boundaries. - Contextual logging (
self.log) on use cases, handlers and aggregates. - Central configuration (
configure(...)) for id / correlation-id factories.
Optional integrations (extras; the core stays dependency-free), grouped under
domino.integrations:
domino.integrations.sqlalchemy(domino[sqlalchemy]) — infrastructure layer
via SQLAlchemy 2.0 imperative mapping, so aggregates stay pristine. Sync
and async repositories, unit of work and aFilterablemixin;
composable specifications that drive both an in-memory check and a SQL
WHEREclause;DomainIdType. The async unit of work can publish domain
events after commit.domino.integrations.fastapi(domino[fastapi]) — presentation layer: a
per-request unit of work, a correlation-id middleware,DomainError→ HTTP
status mapping, and query-params → specifications — all via one
install_dominocall.
Docs & examples
MkDocs documentation site (DDD primer + build-with-Domino guide) and runnable
examples for the core domain, sync/async SQLAlchemy, and FastAPI.
Install
uv add domino # core only
uv add "domino[sqlalchemy]" # + SQLAlchemy integration
uv add "domino[fastapi]" aiosqlite # + FastAPI (async)MIT licensed.