pydomain — DDD, CQRS & Event Sourcing Building Blocks for Python
pydomain is a Python 3.12+ library that provides the tactical and architectural building blocks for building systems using Domain-Driven Design (DDD), Command-Query Responsibility Segregation (CQRS), and Event Sourcing (ES) — individually or composed together.
Design Philosophy
Opinionated about patterns, unopinionated about infrastructure.
pydomain gives you the abstractions, base classes, and wiring for DDD/CQRS/ES patterns. You provide the domain model and the storage adapters. The library never locks you into a specific database, message broker, or web framework.
This means:
- Domain layer has zero infrastructure imports. Your entities, value objects, and aggregates depend only on
pydantic,uuid-utils, and the standard library. This holds for what the layer uses; until 0.4.0, importingpydomain.dddstill loads the other layers too (DDD-17, W2-11). - Infrastructure is pluggable. The
RepositoryandEventStorecontracts are defined astyping.Protocol— any class with the right methods conforms. No inheritance required. - You adopt incrementally. Use Level 1 (DDD only) for a rich domain model on top of a traditional ORM. Add CQRS, the message bus (with sagas and the outbox), and Event Sourcing as your system grows.
The Five Adoption Levels
You don't need all five. Start with what you need and add levels as your system grows.
| Level | What You Use | What You Get |
|---|---|---|
| 1. Tactical DDD | ValueObject, Entity, AggregateRoot, Repository, DomainEvent |
Persistence-ignorant domain model with explicit consistency boundaries |
| 2. + CQRS | Level 1 + Command[TResult], Query[TResult], CommandBus, QueryBus |
Separated read and write paths with typed result abstractions |
| 3. + Message Bus | Level 2 + MessageBus, UnitOfWork, SagaManager, OutboxStore |
Event-driven side effects; sagas; reliable integration-event delivery; inter-aggregate eventual consistency |
| 4. + Event Sourcing | Level 3 + EventSourcedAggregateRoot, EventStore, Projection |
Full audit trail; rebuildable state; multiple read models |
| 5. + Advanced ES | Level 4 + SnapshotStore, UpcasterRegistry, SubscriptionRunner |
Production-grade event sourcing with operational maturity |
Moving up a level adds capabilities without rewriting what already works. A ValueObject at Level 1 is the same class at Level 5. An AggregateRoot becomes an EventSourcedAggregateRoot by changing the base class — the command methods and invariants stay the same.
Built on Pydantic v2
Every domain concept (entities, value objects, events, commands, queries) is a Pydantic BaseModel. This gives you:
- Built-in validation — field constraints and custom validators express domain invariants directly in the type system.
- Serialization for free —
model_dump()andmodel_validate()give round-trip serialization for event stores, message brokers, and API boundaries. - Type safety — generic base classes like
Entity[TId],Command[TResult], andRepository[T, TId]give you typed results with no casting. - JSON Schema generation — self-documenting event and command catalogs for cross-team integration contracts.
Minimal Dependencies
pydomain has only two runtime dependencies:
| Package | Version | Purpose |
|---|---|---|
pydantic |
≥ 2.12.5 | Type system, validation, serialization |
uuid-utils |
≥ 0.10 | Fast UUIDv7 generation via C extension |
Everything else — your database driver, web framework, message broker — is your choice.
Installation
pip install pydomain-lib
Requires Python 3.12+. Pulls in pydantic>=2.12.5 and uuid-utils>=0.10 automatically.
Upgrading? Read How to Upgrade to 0.3.1 first: it lists every behaviour change since 0.3.0, starting with the Pydantic floor.
For development tooling (pytest, ruff, mypy, pre-commit):
pip install "pydomain-lib[dev]"
To run the Protocol contract suites (pydomain.testing.contracts) against your own adapters, install the testing extra. It adds pytest>=8.0 and anyio>=4.0, which only the suites import:
pip install "pydomain-lib[testing]"
Note: The package is installed as
pydomain-libbut imported aspydomain. All code examples below useimport pydomain.
Quick Start — Your First Aggregate in 5 Minutes
from uuid import UUID
from pydomain.ddd.value_object import ValueObject
from pydomain.ddd.aggregate_root import AggregateRoot
from pydomain.ddd.domain_event import DomainEvent
from pydomain.testing.fake_repository import FakeRepository
# 1. Define a Value Object (immutable, defined by its attributes)
class Money(ValueObject):
amount: int
currency: str
def add(self, other: "Money") -> "Money":
if self.currency != other.currency:
raise ValueError("Cannot add different currencies")
return type(self)(amount=self.amount + other.amount, currency=self.currency)
# 2. Define a Domain Event (past-tense fact)
class OrderPlaced(DomainEvent):
order_id: UUID
total_amount: int
currency: str
# 3. Define an Aggregate Root (consistency boundary, owns events)
class Order(AggregateRoot[UUID]):
customer_id: UUID
total: Money
status: str = "pending"
def place(self) -> None:
if self.status != "pending":
raise ValueError("Order is not pending")
self.status = "placed"
self._add_event(OrderPlaced(
order_id=self.id,
total_amount=self.total.amount,
currency=self.total.currency,
))
# 4. Create and use it — id is auto-generated (UUIDv7)
order = Order(
customer_id=UUID("550e8400-e29b-41d4-a716-446655440000"),
total=Money(amount=1000, currency="EUR"),
)
order.place() # records OrderPlaced event
events = order.pull_events() # collect recorded events
print(f"Status: {order.status}") # "placed"
# 5. Test with fakes — no database needed
async def main():
repo: FakeRepository[Order, UUID] = FakeRepository()
await repo.save(order)
found = await repo.get_by_id(order.id)
assert found is not None and found.status == "placed"
Documentation
| Section | Mode | Content |
|---|---|---|
| Getting Started | Tutorial | Installation and quickstart walkthrough |
| Concepts | Explanation | The why behind DDD, CQRS, ES, Sagas, and infrastructure patterns |
| How-To Guides | Task-oriented | Step-by-step recipes for defining aggregates, configuring buses, implementing projections, and more |
| Recipes | Integration | End-to-end patterns combining multiple modules (DDD-only, CQRS+ES, Saga orchestration) |
| API Reference | Reference | Auto-generated module and class index |
Development
git clone https://github.com/mgourlis/pydomain.git
cd pydomain
# Using pip (standard)
pip install -e ".[dev]"
# Or using uv (faster alternative)
uv sync --extra dev
The make targets call python -m …, so run them inside the activated environment or as uv run make test.
| Command | Purpose |
|---|---|
make test |
Run tests with coverage |
make lint |
Lint with ruff |
make format |
Format with ruff |
make type |
Static type checking with mypy |
make check |
Run lint + type checks |
make mutate MODULE=src/pydomain/<path>.py |
Mutation-test one module |
References
- Domain-Driven Design — Evans (2003)
- Patterns of Enterprise Application Architecture — Fowler (2002)
- Event Sourcing — Fowler
- CQRS — Fowler
- Diátaxis Documentation Framework
License
MIT License — see LICENSE file for details.
Metadata
Release files for pydomain-lib 0.3.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| pydomain_lib-0.3.1.tar.gz | 1.2 MB | With provenance | Details for pydomain_lib-0.3.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| pydomain_lib-0.3.1-py3-none-any.whl | Python 3 | none | any | Details for pydomain_lib-0.3.1-py3-none-any.whl |
Total release size: 1.4 MB
Release files / pydomain_lib-0.3.1.tar.gz
| Download URL | pydomain_lib-0.3.1.tar.gz |
|---|---|
| Size | 1.2 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f9a1b43ee89605b2a4475481275e3d45d8c77497cda44e1afaaab2771adfbccb
|
|
BLAKE2b-256 checksum How to use checksums |
16a90bcd888971c29babebb54b21464294840da54e5da39c29586936b041c706
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logRelease files / pydomain_lib-0.3.1-py3-none-any.whl
| Download URL | pydomain_lib-0.3.1-py3-none-any.whl |
|---|---|
| Size | 158.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
125f7f9316c65eadc6d0686da8916f6e837a54c4507a2e098bbddec290f4a40c
|
|
BLAKE2b-256 checksum How to use checksums |
3cb9d4f952f9718f7191fae1945fb8e5c9592519be86967d21912ec5d888edfa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logSecurity for pydomain-lib 0.3.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for pydomain_lib-0.3.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.