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
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, 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 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.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]"
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 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
# Using pip (standard)
pip install -e ".[dev]"
# Or using uv (faster alternative)
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
- 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.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pydomain_lib-0.3.0.tar.gz | 724.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pydomain_lib-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 829.0 kB
Release files / pydomain_lib-0.3.0.tar.gz
| Download URL | pydomain_lib-0.3.0.tar.gz |
|---|---|
| Size | 724.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a9782444f6b924cbf9f49fb3d73ffb5e1626718bfa211a95a88a88446d5b42b0
|
|
BLAKE2b-256 checksum How to use checksums |
cfb23c109c9064d1b550d7f188e4ff7fcc5f7315236fad29980e64228d1d6346
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Sep 30, 2026.
Transparency logRelease files / pydomain_lib-0.3.0-py3-none-any.whl
| Download URL | pydomain_lib-0.3.0-py3-none-any.whl |
|---|---|
| Size | 104.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8bae4e5dab12cc5130b69daa3b3c6143bdab27a0d3f92fada6a47b0f136bbdfb
|
|
BLAKE2b-256 checksum How to use checksums |
2eaa154bcc8270bbcde7e7926ca2f2bfb6e4fba16aa6b189462dc2b33478c5b5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Sep 30, 2026.
Transparency log