Skip to main content

pydomain — DDD, CQRS & Event Sourcing Building Blocks for Python

Ask DeepWiki PyPI version Python versions License Tests Ruff

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, importing pydomain.ddd still loads the other layers too (DDD-17, W2-11).
  • Infrastructure is pluggable. The Repository and EventStore contracts are defined as typing.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() and model_validate() give round-trip serialization for event stores, message brokers, and API boundaries.
  • Type safety — generic base classes like Entity[TId], Command[TResult], and Repository[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-lib but imported as pydomain. All code examples below use import 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

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)

Source distribution for pydomain-lib 0.3.1
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)

Table of built distributions (wheels) for pydomain-lib 0.3.1
File Interpreter ABI Platform Actions
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 log

Release 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 log

Security 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.

Select from 2 files

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 log

A 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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page