Skip to content

Domain Layer

The domain layer is the heart of your application. It contains the business logic, rules, and data structures that define your domain.

Building Blocks

Block Description Mutability
Entity Mutable object with identity Mutable
RootEntity Aggregate root entity Mutable
Identity Field Identity via Field(id=True) -
ValueObject Immutable, identity-less object Immutable
Service Stateless domain operation Stateless
Event Record of something that happened Immutable
Invariants Business rules enforced at construction (@field_invariance, @invariance) -

Imports

from aod.domain import (
    Entity,
    RootEntity,
    ValueObject,
    Service,
    Field,
    PrivateField,
    BoundedContext,
    DomainException,
)
from aod.events import Event, EventCollector

Quick Example

from aod.domain import RootEntity, ValueObject, Field
from aod.events import Event

# Value Object — immutable
class Money(ValueObject):
    amount: float
    currency: str

# Event — immutable, auto-timestamped
class OrderPlaced(Event):
    order_id: int
    total: float

# Root Entity — mutable, has identity
class Order(RootEntity):
    id: int = Field(id=True)
    total: Money

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

Key Concepts

Mutation Guards

Entities and Root Entities have automatic mutation guards:

  • Inside methods: Mutation is allowed (PASS state)
  • Outside methods: Mutation is blocked (BLOCK state)
  • During __init__: Mutation is allowed (INHERIT state)
class User(RootEntity):
    id: int = Field(id=True)
    name: str

    def rename(self, new_name: str) -> None:
        self.name = new_name  # Works!

user = User(id=1, name="Alice")
user.name = "Bob"  # MutationForbiddenException!

Immutable Proxies

When you read attributes outside a mutation context, you get immutable proxies:

user = User(id=1, tags=["admin", "user"])
user.tags.append("super")  # MutationForbiddenException!

Business Invariants

Enforce domain rules at construction time with @field_invariance (field-level) and @invariance (model-level). Violations raise InvarianceException, a domain exception.

from aod.domain.validation import field_invariance


class Money(ValueObject):
    amount: float
    currency: str

    @field_invariance("amount")
    def amount_must_be_positive(cls, v: float) -> float:
        if v < 0:
            raise ValueError("Amount must be positive")
        return v


Money(amount=-5.0, currency="USD")  # InvarianceException!

Event Collection

Events are automatically collected across aggregate boundaries:

from aod.events import EventCollector

with EventCollector() as events:
    order.place()
    order.ship()
# events contains OrderPlaced and OrderShipped

Next Steps

Entity & RootEntity

Learn about mutable domain objects

ValueObject

Learn about immutable domain objects

Service

Learn about stateless domain operations

Event System

Learn about domain events

Bounded Context

Learn about organizing your domain

Invariants

Learn about business rules and validation