Contracts¶
Contracts are immutable data classes for commands (writes) and queries (reads). They define the typed interface for application operations and enforce constraints at class creation time.
Import¶
Class Reference¶
Command[TEntity, TResult]¶
Represents a request to change state. Always immutable.
Type Parameters:
| Parameter | Constraint | Description |
|---|---|---|
TEntity |
Must be a RootEntity subclass |
The root entity this command operates on |
TResult |
Any type | The return type of the command handler |
Constructor: All declared fields become keyword constructor parameters (auto-generated by Pydantic).
Validation at class creation:
TEntitymust be aRootEntitysubclass — raisesInvalidRootEntityTypeErrorotherwise- Field types must not reference non-root
Entityclasses — raisesInvalidCommandFieldTypeErrorotherwise - Even nested references (e.g.,
list[Entity],Optional[Entity]) are checked
from aod.application import Command
class CreateUser(Command[User, None]):
user_id: str
name: str
email: str
# Constructor takes all fields as keyword arguments
cmd = CreateUser(user_id="1", name="Alice", email="alice@example.com")
# Immutable — any mutation raises MutationForbiddenException
cmd.name = "Bob" # MutationForbiddenException!
Parameters (constructor):
| Parameter | Type | Description |
|---|---|---|
*fields |
As declared | Every declared field becomes a required or optional keyword argument |
| (default values) | As declared | Fields with = value are optional in the constructor |
Query[TEntity, TResult]¶
Represents a request to read state. Always immutable.
Type Parameters:
| Parameter | Constraint | Description |
|---|---|---|
TEntity |
Must be a RootEntity subclass |
The root entity this query reads from |
TResult |
Must contain at least one RootEntity |
The return type, validated to include a RootEntity |
Validation at class creation:
TEntitymust be aRootEntitysubclass — raisesInvalidRootEntityTypeErrorotherwiseTResultmust contain at least oneRootEntitytype — raisesInvalidQueryResultTypeErrorotherwise- Field types must not reference non-root
Entityclasses — raisesInvalidCommandFieldTypeErrorotherwise
from aod.application import Query
class GetUser(Query[User, User | None]):
user_id: str
query = GetUser(user_id="1")
query.user_id = "2" # MutationForbiddenException!
Parameters (constructor):
| Parameter | Type | Description |
|---|---|---|
*fields |
As declared | Every declared field becomes a required or optional keyword argument |
| (default values) | As declared | Fields with = value are optional in the constructor |
Type Parameter Rules¶
TEntity: Must be RootEntity¶
Both Command and Query require TEntity to be a RootEntity subclass:
class User(RootEntity):
id: str
name: str
# Valid
class CreateUser(Command[User, None]):
user_id: str
class NonRootEntity(Entity):
id: str
# Invalid — NonRootEntity is not a RootEntity
class BadCommand(Command[NonRootEntity, None]): # InvalidRootEntityTypeError!
pass
TResult: Must contain RootEntity (Query only)¶
Query requires TResult to include at least one RootEntity type:
# Valid — User is a RootEntity
class GetUser(Query[User, User | None]):
user_id: str
class ListUsers(Query[User, list[User]]):
pass
class GetUserWithStats(Query[User, tuple[User, int]]):
user_id: str
# Invalid — int does not contain a RootEntity
class BadQuery(Query[User, int]): # InvalidQueryResultTypeError!
pass
Field Validation¶
Contract fields are validated at class creation. Any field type that references a non-root Entity (even nested in generics) raises InvalidCommandFieldTypeError:
# Valid — str is not an Entity
class CreateUser(Command[User, None]):
user_id: str
name: str
# Invalid — User is a non-root Entity in the field
class CreateUser(Command[User, None]):
user: User # InvalidCommandFieldTypeError!
# Also invalid — Entity nested in generics
class CreateOrder(Command[Order, None]):
items: list[OrderItem] # InvalidCommandFieldTypeError! (if OrderItem is non-root Entity)
Complex Result Types¶
Queries support complex return types as long as at least one RootEntity is present:
# Tuple result
class GetUserWithStats(Query[User, tuple[User, int]]):
user_id: str
# Optional result
class FindUser(Query[User, User | None]):
email: str
# List result
class SearchUsers(Query[User, list[User]]):
query: str
limit: int = 10
# Union with RootEntity
class FindUserOrOrder(Query[User, User | Order]):
identifier: str
Immutability¶
Both Command and Query are always immutable. Any attempt to modify a field raises MutationForbiddenException:
cmd = CreateUser(user_id="1", name="Alice", email="alice@example.com")
cmd.name = "Bob" # MutationForbiddenException!
query = GetUser(user_id="1")
query.user_id = "2" # MutationForbiddenException!
Testing¶
Use the build() helper to construct contracts without validation:
from aod.testing import build
# Build without validation — useful for test setup
cmd = build(CreateUser, user_id="1", name="Alice", email="alice@example.com")
# Or construct normally — validates all constraints
cmd = CreateUser(user_id="1", name="Alice", email="alice@example.com")
assert cmd.user_id == "1"
Common Patterns¶
CRUD Commands¶
class CreateUser(Command[User, None]):
user_id: str
name: str
email: str
class UpdateUser(Command[User, None]):
user_id: str
name: str | None = None
email: str | None = None
class DeleteUser(Command[User, None]):
user_id: str
CRUD Queries¶
class GetUser(Query[User, User | None]):
user_id: str
class ListUsers(Query[User, list[User]]):
pass
class SearchUsers(Query[User, list[User]]):
query: str
limit: int = 10
Domain-Specific Commands¶
class PlaceOrder(Command[Order, None]):
order_id: str
items: list[OrderItem]
class ShipOrder(Command[Order, None]):
order_id: str
tracking_number: str
class CancelOrder(Command[Order, None]):
order_id: str
reason: str