Skip to main content

DDD, CQRS & Event Sourcing Building Blocks library for Python

Project description

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, datetime, and the standard library.
  • 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, Event Sourcing, and Sagas 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 Event-driven side effects; 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, Upcaster, 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 freemodel_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.7 Type system, validation, serialization
uuid-utils ≥ 0.9 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.7 and uuid-utils>=0.9 automatically.

For development tooling (pytest, ruff, mypy, pre-commit):

pip install "pydomain-lib[dev]"

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 self.model_copy(update={"amount": self.amount + other.amount})


# 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
uv sync --extra dev
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 all checks (lint + type + test)

References

License

MIT License — see LICENSE file for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pydomain_lib-0.2.0.tar.gz (527.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pydomain_lib-0.2.0-py3-none-any.whl (89.8 kB view details)

Uploaded Python 3

File details

Details for the file pydomain_lib-0.2.0.tar.gz.

File metadata

  • Download URL: pydomain_lib-0.2.0.tar.gz
  • Upload date:
  • Size: 527.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pydomain_lib-0.2.0.tar.gz
Algorithm Hash digest
SHA256 96aff838652e74fa6699490f4be1602b271469df68768bc166bf2f2f96fce43f
MD5 b8e6320aed6d20296ed5b68d19deba64
BLAKE2b-256 25424092c63ae79f10f7980cbe9bad0452aa8ce46c3ccae3d29d4225fd6f3664

See more details on using hashes here.

Provenance

The following attestation bundles were made for pydomain_lib-0.2.0.tar.gz:

Publisher: publish_to_pypi.yml on mgourlis/pydomain

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pydomain_lib-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: pydomain_lib-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 89.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pydomain_lib-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d8c4ec3bfaba471c291d55d2ad61939ceb562fd4fa86784b35b2553576d4b5f1
MD5 9945d79a6dd7a364eb67199900a6288f
BLAKE2b-256 57f163d55fbb7637d8d6df612f4d9e5354ad002d3366e701fa207609875dc680

See more details on using hashes here.

Provenance

The following attestation bundles were made for pydomain_lib-0.2.0-py3-none-any.whl:

Publisher: publish_to_pypi.yml on mgourlis/pydomain

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page