Skip to content

UseCase

UseCases orchestrate domain objects through Ports. They are the central building block of the application layer, handling transaction management, logging, and event publishing automatically.

Commands and Queries are internal — created by the UseCase, not passed by the caller. Use Pydantic BaseModel subclasses for run() input.

Import

from aod.application import UseCase
from pydantic import BaseModel

For async operations:

from aod.application.async_ import UseCase

Basic Usage

Subclass UseCase, define CommandPort[Command] and QueryPort[Query] fields, and implement the run() method.

from aod.application import UseCase, CommandPort, Command
from pydantic import BaseModel

class CreateUserInput(BaseModel):
    user_id: str
    name: str
    email: str

class CreateUser(Command[User, None]):
    user_id: str
    name: str
    email: str

class CreateUserUseCase(UseCase):
    save_user: CommandPort[CreateUser]

    def run(self, dto: CreateUserInput) -> User:
        user = User(id=dto.user_id, name=dto.name, email=dto.email)
        self.save_user.handle(CreateUser(
            user_id=user.id, name=user.name, email=user.email,
        ))
        self._event_emitter.emit(UserCreated(user_id=dto.user_id))
        return user
uc = container.adapt(CreateUserUseCase)
user = uc.run(CreateUserInput(user_id="1", name="Alice", email="alice@example.com"))

Class Reference

UseCase

Base class for synchronous use cases. Inherits from BaseOperation.

Parameters (constructor — auto-generated by Pydantic based on declared fields):

Parameter Type Description
*port_fields Port subclass All declared Port fields as keyword arguments

Auto-wired fields:

Field Type Default Description
events list[Event] [] Collected events from last run() call. Read-only outside mutation context
_event_emitter EventEmitter EventEmitter() Private event emitter for emitting events during run()
_loggers list[Logger \| AsyncLogger] [] Private list of declared logger ports
_event_buses list[EventBus \| AsyncEventBus] [] Private list of declared event bus ports

Optional ports (declare explicitly when needed):

class CreateUserUseCase(UseCase):
    save_user: CommandPort[CreateUser]
    logger: Logger
    event_bus: EventBus

    def run(self, dto: CreateUserInput) -> User:
        ...

run(self, *args, **kwargs) -> Any

Abstract method. Subclasses define specific parameters. Values are passed here, not as class fields. The method is automatically wrapped to:

  1. Begin an internal Transaction
  2. Open an EventCollector context
  3. Invoke the original run() body
  4. Collect emitted events into self.events
  5. On success: commit Transaction (flushing caches internally), log completion, publish events
  6. On failure: rollback Transaction, log error, re-raise

Parameters: Defined by the subclass — any number of positional and keyword arguments representing input values.

Returns: Any return value defined by the subclass.

AsyncUseCase

Base class for asynchronous use cases. Inherits from BaseOperation.

Parameters (constructor):

Parameter Type Description
*port_fields Port subclass All declared Port fields as keyword arguments

Auto-wired fields: Same as UseCase.

async run(self, *args, **kwargs) -> Any

Async abstract method. Same wrapping behavior as sync run() but bridges sync/async calls via should_await internally.

Field Validation

All declared fields on a UseCase must be Port subclasses. Non-Port fields raise InvalidUseCasePortFieldError:

class CreateUser(UseCase):
    user_id: str  # InvalidUseCasePortFieldError — not a Port
    name: str     # InvalidUseCasePortFieldError — not a Port

Blocked field types (rejected even if they are Port-like):

Type Reason
Session UseCases should not depend on sessions directly
AsyncSession UseCases should not depend on sessions directly
BaseHandler Handlers belong in infrastructure
AsyncBaseHandler Handlers belong in infrastructure

Use CommandPort[Command] or QueryPort[Query] instead:

from aod.application import CommandPort, Command

class CreateUser(Command[User, None]):
    user_id: str
    name: str

class CreateUserUseCase(UseCase):
    save_user: CommandPort[CreateUser]

    def run(self, user_id: str, name: str) -> None:
        user = User(id=user_id, name=name)
        self.save_user.handle(CreateUser(user_id=user_id, name=name))

Event Collection

Events emitted during run() are automatically collected. This includes events emitted directly by the UseCase via self._event_emitter.emit(...) and events emitted by any entity, value object, or service touched during execution.

class CreateUserUseCase(UseCase):
    save_user: CommandPort[CreateUser]

    def run(self, dto: CreateUserInput) -> None:
        user = User(id=dto.user_id)
        user.register()  # emits UserRegistered
        self.save_user.handle(CreateUser(user_id=user.id, name=dto.name, email=dto.email))
        self._event_emitter.emit(UserCreated(user_id=dto.user_id))

uc = CreateUserUseCase(save_user=handler)
uc.run(CreateUserInput(user_id="1", name="Alice", email="alice@example.com"))
assert len(uc.events) == 2  # UserRegistered + UserCreated
assert isinstance(uc.events[0], UserRegistered)
assert isinstance(uc.events[1], UserCreated)

The wrapper: 1. Opens an EventCollector context before run() executes 2. Collects all emitted events into self.events 3. Publishes events on the event bus after a successful commit 4. Replaces self.events on each new call to run()

If run() raises an exception, collected events are discarded and self.events is cleared:

Private Methods

UseCases can have private helper methods:

class CreateUserUseCase(UseCase):
    save_user: CommandPort[CreateUser]

    def _validate(self, name: str) -> bool:
        return len(name) > 0

    def run(self, dto: CreateUserInput) -> None:
        if not self._validate(dto.name):
            raise ValueError("Invalid name")
        user = User(id=dto.user_id, name=dto.name)
        self.save_user.handle(CreateUser(user_id=user.id, name=dto.name, email=dto.email))

Error Handling

The auto-wrapper handles errors:

  1. If run() raises: Transaction is rolled back, error is logged, exception is re-raised
  2. If commit() fails: Transaction is rolled back, error is logged, exception is re-raised
class CreateUserUseCase(UseCase):
    save_user: CommandPort[CreateUser]

    def run(self) -> None:
        raise ValueError("Something went wrong")

uc = CreateUserUseCase(save_user=handler)
try:
    uc.run()
except ValueError:
    pass

assert uc.events == []  # Events cleared on failure

Testing

Use spy_adapter_container for testing use cases instead of mocking ports manually:

from aod.testing.doubles import spy_adapter_container

container = spy_adapter_container(AdapterContainer(sessions={MySession}, handlers=[CreateUserHandler]))
use_case = container.adapt(CreateUserUseCase)

use_case.run(CreateUserInput(user_id="1", name="Alice", email="alice@example.com"))

assert use_case.events
assert container.get_handler(CreateUser).handle.called

Common Patterns

Command Use Case

class PlaceOrderInput(BaseModel):
    order_id: str
    total: float

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

    def run(self, dto: PlaceOrderInput) -> None:
        order = Order(id=dto.order_id, total=dto.total)
        self.place_order.handle(PlaceOrder(order_id=dto.order_id, total=dto.total))
        self._event_emitter.emit(OrderPlaced(order_id=dto.order_id))

Query Use Case

class GetUserInput(BaseModel):
    user_id: str

class GetUserUseCase(UseCase):
    get_user: QueryPort[GetUser]

    def run(self, dto: GetUserInput) -> User | None:
        return self.get_user.handle(GetUser(user_id=dto.user_id))

Use Case with Custom Service Port

For non-database dependencies (API clients, notifications), use a custom Port subclass:

from aod.application import Port


class NotificationClient(Port):
    def send_email(self, to: str, subject: str, body: str) -> None: ...


class NotifyUserInput(BaseModel):
    user_id: str
    message: str

class NotifyUser(UseCase):
    notification: NotificationClient

    def run(self, dto: NotifyUserInput) -> None:
        user = User(id=dto.user_id)
        self.notification.send_email(to=user.email, subject="Alert", body=dto.message)

Use Case with Validation

Validation belongs in the domain, not the use case. Define constraints on the entity and let Pydantic enforce them:

from aod.domain.validation import field_invariance


class User(RootEntity):
    id: str
    name: str
    email: str

    @field_invariance("name")
    def name_required(cls, v: str) -> str:
        if not v:
            raise ValueError("Name is required")
        return v

    @field_invariance("email")
    def email_valid(cls, v: str) -> str:
        if "@" not in v:
            raise ValueError("Invalid email")
        return v


class CreateUserUseCase(UseCase):
    save_user: CommandPort[CreateUser]

    def run(self, dto: CreateUserInput) -> None:
        user = User(id=dto.user_id, name=dto.name, email=dto.email)
        self.save_user.handle(CreateUser(user_id=user.id, name=user.name, email=user.email))

Next Steps

Port

Learn how ports define interfaces

Contracts

Learn about commands and queries

Handlers

Learn about command/query handlers

Container

Wire dependencies into use cases and projections