Skip to main content

DDD building blocks: entities, value objects, bounded contexts, and validation helpers.

Project description

attack-on-domain

Domain-Driven Design building blocks for Python 3.14+ — entities, value objects, aggregates, CQRS, ports and adapters, use cases, domain events, invariants, and dependency injection. All running on Pydantic v2, fully typed, with mutation guards that actually work.

No ORM. No framework lock-in. No "just pip install django and pray."


Why This Exists

You have a complex domain. Your codebase is turning into a big ball of mud. Your entities are anemic, your services are god objects, and somehow User extends AbstractBaseModelMixinFactory in six different ways.

attack-on-domain gives you real DDD primitives:

  • Entities with enforced identity fields (one, exactly one, or the class doesn't compile)
  • Value Objects that are actually immutable — no @property hacks, no __setattr__ tricks
  • Root Entities that know they're the aggregate root and refuse to be nested inside anything else
  • Mutation guards that block writes from outside your methods (user.name = "hacker" raises MutationForbiddenException — try that with a dataclass)
  • Business invariants via decorators that turn ValueError into InvarianceException at construction time
  • CQRS that's built-in, not bolted on — Command[TEntity, TResult] and Query[TEntity, TResult] with compile-time generic validation
  • Event collection that Just Works — emit from entities, VOs, services; UseCases collect them automatically; EventCollector context manager catches cross-aggregate events
  • Ports & adaptersCommandPort[T] / QueryPort[T] on UseCases, infrastructure handlers implement them, AdapterContainer wires everything. No service locator, no global state, no singletons.

AI-Native by Design

LLMs are great at generating code. They're terrible at maintaining implicit invariants. This framework makes invariants explicit:

  • Every entity identity is declared up front — the LLM can't "forget" which field is the ID
  • Every mutation boundary is enforced at runtime — the LLM can't accidentally write self.total = 0 outside a method
  • Every event emission is tracked — the LLM can't emit events that go nowhere
  • Every port is an interface — the LLM generates adapters against a contract, not implementation details

The result: an AI agent can safely generate, refactor, and extend your domain code without silently breaking business rules. Try that with a plain Pydantic model.

Built-in Agent Skill

This repo ships with a skill at skills/attack-on-domain/ — a comprehensive reference that teaches agents how to use the library correctly. Load it with /load attack-on-domain or install it with skills library:

npx skills add alfonsocuesta/attack-on-domain

It will ask questions you haven't thought of. It's annoying on purpose. Your domain will be better for it.

Quick Example

from aod.domain import RootEntity, ValueObject, Field
from aod.events import Event
from aod.application import UseCase, Command, CommandPort
from aod.infrastructure import CommandHandler, Session, AdapterContainer


class SqlSession(Session):
    def execute(self, operation: object) -> None: ...
    def begin(self) -> None: ...
    def commit(self) -> None: ...
    def rollback(self) -> None: ...
    def close(self) -> None: ...
    def is_dirty(self) -> bool:
        return False


class OrderId(ValueObject):
    value: str


class OrderPlaced(Event):
    order_id: str
    total: float


class Order(RootEntity):
    id: OrderId = Field(id=True)
    total: float

    def place(self) -> None:
        self._event_emitter.emit(
            OrderPlaced(order_id=self.id.value, total=self.total)
        )


class PlaceOrder(Command[Order, None]):
    order_id: str
    total: float


class PlaceOrderHandler(CommandHandler[PlaceOrder]):
    session: SqlSession

    def handle(self, command: PlaceOrder) -> None:
        self.session.execute(command)


class PlaceOrderUseCase(UseCase):
    place_order: CommandPort[PlaceOrder]

    def run(self, order_id: str, total: float) -> None:
        order = Order(id=OrderId(value=order_id), total=total)
        order.place()
        self.place_order.handle(PlaceOrder(order_id=order_id, total=total))


container = AdapterContainer(
    sessions={SqlSession},
    handlers=[PlaceOrderHandler],
)
use_case = container.adapt(PlaceOrderUseCase)
use_case.run(order_id="1", total=99.99)
# Events are auto-collected: use_case.events -> [OrderPlaced(...)]

FastAPI? You Bet.

Because the framework keeps infrastructure out of your domain, wiring it into FastAPI is a few lines:

from functools import lru_cache
from fastapi import FastAPI, Depends
from aod.application import UseCase
from aod.application.async_ import UseCase as AsyncUseCase
from aod.infrastructure import AdapterContainer

from yourdomain import CreateUserUseCase, CreateUserInput
from yourinfra import PostgresSession, CreateUserHandler

app = FastAPI()

@lru_cache
def get_container() -> AdapterContainer:
    return AdapterContainer(
        sessions={PostgresSession},
        handlers=[CreateUserHandler],
    )

def get_use_case(
    use_case: type[UseCase | AsyncUseCase],
    container: AdapterContainer = Depends(get_container),
):
    return container.adapt(use_case)

@app.post("/users")
def create_user(
    payload: CreateUserInput,
    use_case: CreateUserUseCase = Depends(get_use_case(CreateUserUseCase)),
):
    return use_case.run(payload)

The same use case works with CLI scripts, background workers, and tests. No @app decorators in your domain. No async def leaks into entities. Just ports, adapters, and a container.

Install

uv add attack-on-domain

Or with pip:

pip install attack-on-domain

Requires Python 3.14+.

Documentation

Full docs at alfonsocuesta.github.io/attack-on-domain

You Want To Go Here
Install & first 5 minutes Getting Started
Learn the building blocks Domain Layer
Orchestrate with use cases Application Layer
Write database adapters Infrastructure Layer
Test without mocks Testing
Map DDD concepts to code DDD to AoD
Full API reference API Reference

License

Apache 2.0

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

attack_on_domain-0.0.33.tar.gz (60.4 kB view details)

Uploaded Source

Built Distribution

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

attack_on_domain-0.0.33-py3-none-any.whl (98.7 kB view details)

Uploaded Python 3

File details

Details for the file attack_on_domain-0.0.33.tar.gz.

File metadata

  • Download URL: attack_on_domain-0.0.33.tar.gz
  • Upload date:
  • Size: 60.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for attack_on_domain-0.0.33.tar.gz
Algorithm Hash digest
SHA256 3414b8ad6180b7ad8659d044581e6808f9dfc8580e991e97d80c4db777b3b300
MD5 86792bf151752e3649ad497d702f193d
BLAKE2b-256 8c64b48cc330eadfcd5d1a7d194302e73db62b0b03222a72f030b9bf05e8f1b3

See more details on using hashes here.

File details

Details for the file attack_on_domain-0.0.33-py3-none-any.whl.

File metadata

  • Download URL: attack_on_domain-0.0.33-py3-none-any.whl
  • Upload date:
  • Size: 98.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for attack_on_domain-0.0.33-py3-none-any.whl
Algorithm Hash digest
SHA256 69a7fc99be1c2c8dbc8c4fa894f3e65b789cfa4cf7f989e498102163fd9757ea
MD5 0d98df391dc6719b4f3beda143e7d143
BLAKE2b-256 9b99b7728aea1561986c40e476855cf49af229db0c1cff658047e132d9a941a0

See more details on using hashes here.

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