Quick Start¶
This guide walks through building a user registration system with attack-on-domain in 5 steps.
1. Define Value Objects, Events, and Root Entity¶
Value Objects are immutable, identity-less types. Events record what happened. Root Entities have identity and enforce invariants.
from aod.domain import ValueObject, RootEntity
from aod.events import Event
class Email(ValueObject):
value: str
class UserRegistered(Event):
user_id: str
email: str
class User(RootEntity):
id: str
email: Email
name: str
def register(self) -> None:
self._event_emitter.emit(
UserRegistered(user_id=self.id, email=self.email.value)
)
2. Define a Command and its Handler¶
Commands are immutable requests to change state. Handlers contain the infrastructure logic to execute them.
from aod.application import Command
from aod.infrastructure import CommandHandler
from aod.infrastructure import Session
class InMemorySession(Session):
def begin(self) -> None: ...
def commit(self) -> None: ...
def rollback(self) -> None: ...
def close(self) -> None: ...
def is_dirty(self) -> bool:
return False
class RegisterUser(Command[User, None]):
user_id: str
email: str
name: str
class RegisterUserHandler(CommandHandler[RegisterUser]):
session: InMemorySession
def handle(self, command: RegisterUser) -> None:
user = User(
id=command.user_id,
email=Email(value=command.email),
name=command.name,
)
self.session.execute(user)
3. Create a Use Case with CommandPort¶
Use Cases orchestrate domain logic. They depend on CommandPort[Command] and QueryPort[Query] — NOT custom repository ports. Values flow through run() parameters.
from aod.application import UseCase, CommandPort
class RegisterUserUseCase(UseCase):
save_user: CommandPort[RegisterUser]
def run(self, user_id: str, email: str, name: str) -> None:
user = User(
id=user_id,
email=Email(value=email),
name=name,
)
user.register()
self.save_user.handle(RegisterUser(
user_id=user_id, email=email, name=name,
))
4. Wire It Together¶
Use AdapterContainer and adapt. The container discovers handlers and auto-wires them into the use case.
from aod.infrastructure import AdapterContainer
container = AdapterContainer(handlers=[RegisterUserHandler])
use_case = container.adapt(RegisterUserUseCase)
use_case.run(user_id="2", email="alice@example.com", name="Alice")
5. Test It¶
Use build() to construct objects without validation, events_of() to inspect emitted events, assert_event_emitted() for assertions, and spy_session for handler testing.
from aod.testing import build, events_of, assert_event_emitted
from aod.testing.doubles import SpyLogger, spy_session
user = build(User, id="1", email=Email(value="test@example.com"), name="Test")
user.register()
assert len(events_of(user)) == 1
assert_event_emitted(user, UserRegistered)
# Test handler with a spy session
session = spy_session(MySession)()
handler = RegisterUserHandler(session=session)
handler.handle(RegisterUser(user_id="2", email="alice@example.com", name="Alice"))
assert handler.handle.called
Next Steps¶
DDD Concepts
Understand the theory behind the building blocks
Domain Objects
Detailed API for entities, value objects, and services
Testing Utilities
More testing patterns and assertions
Container
Dependency injection and wiring