Skip to content

Identity Field

Every Entity and RootEntity subclass must have exactly one identity field marked with Field(id=True). The identity field can be any type — int, str, UUID, or a custom type — and the framework enforces the "exactly one" rule at class creation time.

Marking the Identity Field

Use Field(id=True) to mark the identity field:

from aod.domain import RootEntity, Field


class User(RootEntity):
    id: int = Field(id=True)
    name: str
    email: str

Any type works as an identity field:

import uuid
from aod.domain import Entity, Field


class Order(Entity):
    id: uuid.UUID = Field(id=True)
    total: float


class Post(Entity):
    id: str = Field(id=True)
    title: str

ValueObject as Identity

Identity fields can also be ValueObject subclasses, providing type safety and encapsulation:

from aod.domain import RootEntity, ValueObject, Field


class UserId(ValueObject):
    value: str


class User(RootEntity):
    id: UserId = Field(id=True)
    name: str
    email: str

user = User(id=UserId(value="abc-123"), name="Alice", email="alice@example.com")
assert user.id.value == "abc-123"

ValueObject identities are compared by value — two entities with UserId(value="abc-123") are equal regardless of other fields. This is the recommended pattern for rich identity types that carry domain meaning.

Distinguishing Identity from References

When an entity has multiple fields of the same type, Field(id=True) tells the framework which one is the identity:

from aod.domain import RootEntity, Field


class User(RootEntity):
    id: int = Field(id=True)  # This is the identity
    manager_id: int           # This is a reference, not the identity
    name: str

Exactly One Identity Field

The framework requires exactly one field marked with Field(id=True):

class User(RootEntity):
    id: int = Field(id=True)  # OK
    name: str


class BadEntity(Entity):
    name: str  # NoIdentityFieldException at class creation


class BadEntity2(Entity):
    id: int = Field(id=True)
    alt_id: int = Field(id=True)  # TooManyIdentityFieldsException

This check runs at class creation time — not at instantiation — so the error surfaces as soon as the class is defined.

Mutating the ID Changes the Hash

You can change an entity's identity inside a method:

class User(RootEntity):
    id: int = Field(id=True)
    name: str

    def reassign(self, new_id: int) -> None:
        self.id = new_id

However, changing the identity also changes the entity's hash. If the entity is stored in a set or used as a dict key, the hash change can cause subtle bugs:

user = User(id=1, name="Alice")
s = {user}
d = {user: "found"}

user.reassign(2)

assert user in s  # False! Hash changed after insertion
assert user in d  # False! Hash changed after insertion

Best practice: avoid mutating an entity's identity after construction. If you need to change the identity, create a new entity or ensure the entity was never placed in a set/dict before the mutation.

Equality by Identity

Entities compare by their identity field value, not by their other fields:

class User(RootEntity):
    id: int = Field(id=True)
    name: str

u1 = User(id=1, name="Alice")
u2 = User(id=1, name="Bob")
assert u1 == u2  # True — same identity
assert hash(u1) == hash(u2)

Different entity types with the same identity value are never equal:

class Admin(RootEntity):
    id: int = Field(id=True)
    role: str

a = Admin(id=1, role="super")
u = User(id=1, name="x")
assert a != u  # Different types

Exceptions

Exception Raised When
NoIdentityFieldException An Entity or RootEntity subclass has no field marked with Field(id=True)
TooManyIdentityFieldsException An Entity or RootEntity subclass has more than one field marked with Field(id=True)

Next Steps

Entity & RootEntity

Learn about mutable domain objects with identity

ValueObject

Learn about immutable domain objects

Invariants vs __post_init__

Learn when to use invariants and when to use __post_init__