Skip to content

Releases: c-t-n/domino

domino 0.2.0

Choose a tag to compare

@c-t-n c-t-n released this 25 Aug 12:09

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 bus

Events 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 a lambda:. Passing an instance raises a
    TypeError explaining the fix.
  • The async SQLAlchemy unit of work no longer scans the session's identity map
    for aggregates with pending events. Queue them with enqueue_events(...) or
    nothing is published.

Fixes

  • AsyncSqlAlchemyUnitOfWork.rollback called an unset hook, so a rollback never
    reached the session.
  • AsyncUnitOfWork.__getattr__ read self.__dict and 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 of updated_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

Choose a tag to compare

@c-t-n c-t-n released this 14 Aug 10:52

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, DomainId
  • DomainEvent + EventBus / EventHandler (in-memory pub/sub, handler errors
    isolated)
  • Repository[T], UnitOfWork, DomainService
  • Command + UseCase[C, R] / AsyncUseCase[C, R]
  • Result monad and a DomainError hierarchy (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 a Filterable mixin;
    composable specifications that drive both an in-memory check and a SQL
    WHERE clause; 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_domino call.

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.