The domain layer (forze.domain) holds business logic and data structures. It has no knowledge of databases, HTTP, or external services. For the conceptual overview, see Domain Layer. This page is the complete API reference.
Base models
CoreModel
Base for all domain models. Extends Pydantic's BaseModel with:
- Field docstrings enabled for schema generation
- Stripped string fields
- Sorted set serialization for stable JSON output
For example:
from forze.domain.models import CoreModel
class Settings(CoreModel):
name: str
tags: set[str] = set()
BaseDTO
Extends CoreModel with frozen semantics (immutable after creation). Use for command DTOs, update payloads, and read projections:
from forze.domain.models import BaseDTO
class UpdateProjectCmd(BaseDTO):
title: str | None = None
description: str | None = None
Document
Document is the base class for versioned aggregates. Every document carries identity, revision tracking, and timestamps.
from forze.domain.models import Document
class Project(Document):
title: str
description: str = ""
Built-in fields
| Field | Type | Default | Frozen | Purpose |
|---|---|---|---|---|
id |
UUID |
uuid7() |
Yes | Unique identifier |
rev |
int |
1 |
Yes | Revision number (incremented by adapters) |
created_at |
datetime |
utcnow() |
Yes | Creation timestamp |
last_update_at |
datetime |
utcnow() |
No | Last modification timestamp |
Frozen fields raise ValidationError if an update attempts to change them.
update()
Apply a validated update and return the new document plus a computed diff:
project = Project(title="Alpha", description="First")
updated, diff = project.update({"title": "Beta"})
# updated.title == "Beta"
# diff == {"title": "Beta", "last_update_at": <timestamp>}
The update flow:
- Validate — reject unknown and frozen fields
- Compute diff — apply the patch to a JSON dump, calculate the minimal merge patch
- Bump timestamp — set
last_update_atto now - Run validators — execute registered
@update_validatorhooks - Return — new immutable copy and the diff
If the patch produces no changes, returns the original instance and an empty dict.
touch()
Update only last_update_at without changing other fields:
touched, diff = project.touch()
# diff == {"last_update_at": <timestamp>}
validate_historical_consistency()
Check whether applying a patch to an older state would conflict with the current state:
is_safe = current.validate_historical_consistency(
old_state,
incoming_patch,
)
Returns True when the incoming patch does not touch fields that changed between old_state and current. Used by adapters reconstructing state from history to prevent conflicting concurrent merges.
Commands and read models
CreateDocumentCmd
Base for create commands. Frozen DTO that optionally accepts id and created_at for imports and migrations:
from forze.domain.models import CreateDocumentCmd
class CreateProjectCmd(CreateDocumentCmd):
title: str
description: str
| Field | Type | Default | Purpose |
|---|---|---|---|
id |
UUID | None |
None |
Override the auto-generated ID |
created_at |
datetime \| None |
None |
Override the auto-generated timestamp |
ReadDocument
Base for read projections returned by ports. Includes the standard document fields:
from forze.domain.models import ReadDocument
class ProjectRead(ReadDocument):
title: str
description: str
| Field | Type | Purpose |
|---|---|---|
id |
UUID |
Document identifier |
rev |
int |
Revision number |
created_at |
datetime |
Creation timestamp |
last_update_at |
datetime |
Last modification timestamp |
DocumentHistory
Stores a snapshot of a document at a given revision:
from forze.domain.models import DocumentHistory
history_entry: DocumentHistory[Project]
| Field | Type | Purpose |
|---|---|---|
source |
str |
Source table or collection |
id |
UUID |
Document identifier |
rev |
int |
Revision at the time of snapshot |
created_at |
datetime |
When the history entry was created |
data |
D |
Full document snapshot |
Update validators
Validators enforce business rules during Document.update(). Decorate methods with @update_validator to register them:
from forze.domain.validation import update_validator
from forze.base.errors import ValidationError
class Project(Document):
title: str
status: str = "draft"
@update_validator
def _block_published(before, after, diff):
if before.status == "published" and "title" in diff:
raise ValidationError("Cannot change title after publishing.")
Validator signatures
The decorator normalizes three function signatures:
| Parameters | When to use |
|---|---|
(before) |
Only need the state before the update |
(before, after) |
Need both before and after states |
(before, after, diff) |
Need the diff dict as well |
Field-scoped validators
Restrict a validator to specific fields with the fields parameter. The validator only runs when the diff touches at least one of the listed fields:
@update_validator(fields={"status"})
def _validate_transition(before, after, diff):
allowed = {"draft": {"active"}, "active": {"archived"}}
if after.status not in allowed.get(before.status, set()):
raise ValidationError("Invalid status transition.")
Validator collection
Validators are collected from the full class hierarchy at class definition time via collect_update_validators(). Conflict resolution when a subclass redefines a validator name:
on_conflict |
Behavior |
|---|---|
"warn" (default) |
Emit a RuntimeWarning and use the subclass version |
"error" |
Raise TypeError |
"overwrite" |
Silently use the subclass version |
Override the default by setting _update_validators_on_conflict on the class:
class StrictProject(Document):
_update_validators_on_conflict = "error"
Mixins
Reusable domain concerns composed via multiple inheritance. Each mixin adds a focused capability.
SoftDeletionMixin
Adds an is_deleted boolean and a validator that blocks updates to soft-deleted documents (except toggling the flag itself):
from forze.domain.mixins import SoftDeletionMixin
class Project(SoftDeletionMixin, Document):
title: str
Once is_deleted is True, updating any field other than is_deleted raises ValidationError.
| Field | Type | Default |
|---|---|---|
is_deleted |
bool |
False |
NameMixin
Adds a required name and optional display_name, short_name, and description:
from forze.domain.mixins import (
NameMixin,
NameCreateCmdMixin,
NameUpdateCmdMixin,
)
class Workspace(NameMixin, Document):
pass
class CreateWorkspaceCmd(NameCreateCmdMixin, CreateDocumentCmd):
pass
class UpdateWorkspaceCmd(NameUpdateCmdMixin, BaseDTO):
pass
| Field | Type | Required in model | Required in create | Required in update |
|---|---|---|---|---|
name |
String |
Yes | Yes | No (optional) |
display_name |
String | None |
No | No | No |
short_name |
String | None |
No | No | No |
description |
LongString | None |
No | No | No |
NumberMixin
Adds a required positive integer number_id for human-readable identification. Typically populated by a counter adapter during the create mapping step:
from forze.domain.mixins import NumberMixin, NumberCreateCmdMixin
class Ticket(NumberMixin, Document):
title: str
class CreateTicketCmd(NumberCreateCmdMixin, CreateDocumentCmd):
title: str
| Field | Type | Required |
|---|---|---|
number_id |
PositiveInt |
Yes in model and create cmd, optional in update |
CreatorMixin
Adds a frozen creator_id field (UUID). Typically injected by a mapping step that reads the current actor context:
from forze.domain.mixins import CreatorMixin, CreatorCreateCmdMixin
class Comment(CreatorMixin, Document):
body: str
class CreateCommentCmd(CreatorCreateCmdMixin, CreateDocumentCmd):
body: str
| Field | Type | Frozen |
|---|---|---|
creator_id |
UUID |
Yes |
Domain constants
String constants used across layers for consistent field naming:
from forze.domain.constants import ID_FIELD, REV_FIELD, SOFT_DELETE_FIELD
| Constant | Value | Purpose |
|---|---|---|
ID_FIELD |
"id" |
Document identifier field |
REV_FIELD |
"rev" |
Revision field |
SOFT_DELETE_FIELD |
"is_deleted" |
Soft deletion flag |
NUMBER_ID_FIELD |
"number_id" |
Human-readable number |
CREATOR_ID_FIELD |
"creator_id" |
Creator reference |
HISTORY_SOURCE_FIELD |
"source" |
History source field |
HISTORY_DATA_FIELD |
"data" |
History data field |
TENANT_ID_FIELD |
"tenant_id" |
Tenant identifier |