Domain Modeling
How Aleph uses Domain-Driven Design (DDD) in Rust — Entity, AggregateRoot, ValueObject, bounded contexts, and the trait-based architecture that organizes the codebase.
Aleph's codebase is organized using Domain-Driven Design (DDD) principles, implemented through Rust's trait system. Rather than relying on a heavyweight DDD framework, Aleph uses lightweight trait contracts to enforce domain rules at compile time. This page describes the modeling approach, the core domain primitives, and how bounded contexts partition the system into cohesive modules.
DDD in Aleph is not ceremonial — it serves a practical purpose. By making domain concepts explicit in the type system, the codebase communicates its intent clearly and prevents entire categories of bugs through compile-time guarantees.
Why DDD in Aleph
A personal AI assistant is a complex domain. It involves conversations, memory, task orchestration, tool execution, security policies, and multi-channel communication. Without explicit modeling, these concerns blur together, creating a codebase where everything depends on everything.
DDD provides the organizational backbone:
- Bounded contexts prevent cross-domain coupling. The Memory system does not know about teams/dispatcher scheduling, and teams/dispatcher does not know about the Gateway's session management.
- Aggregate roots enforce consistency boundaries. Modifications to related objects go through a single entry point, preventing inconsistent state.
- Value objects eliminate identity confusion. When two objects are equal if and only if their attributes match, making them value objects prevents bugs where identity is accidentally assumed.
- Entities make identity explicit. When an object needs to persist across operations and be referenced by other objects, marking it as an entity makes this contract visible in the code.
Core Primitives
Aleph defines three foundational DDD traits in its domain module. These are minimal by design — they enforce contracts without adding runtime overhead.
Entity
An Entity is an object with a unique identity that persists across state changes. Two entities are equal if and only if they share the same ID, regardless of their other attributes.
pub trait Entity {
type Id: Eq + Clone + std::fmt::Display;
fn id(&self) -> &Self::Id;
}Characteristics:
- Has a unique identifier accessible via
id(). - Identity is stable — the same entity retains its ID across all modifications.
- State can change, but identity cannot.
- Equality is determined solely by ID comparison.
When to use Entity: When an object needs to be tracked across operations, referenced by other objects, or has a lifecycle (created, updated, deleted).
Example:
pub struct Task {
id: String,
name: String,
status: TaskStatus,
dependencies: Vec<String>,
}
impl Entity for Task {
type Id = String;
fn id(&self) -> &Self::Id { &self.id }
}A Task is an entity because it has a lifecycle — it is created, its status changes, dependencies are updated, and eventually it completes or fails. Two tasks with the same data but different IDs are different tasks. Two tasks with the same ID but different statuses are the same task in different states.
AggregateRoot
An AggregateRoot is the entry point of an aggregate — a cluster of related objects that are treated as a unit for consistency purposes. All modifications to objects within the aggregate must go through the aggregate root.
pub trait AggregateRoot: Entity {}Characteristics:
- Inherits all Entity properties (has identity, stable across changes).
- Serves as the transactional boundary — external code interacts with the aggregate only through the root.
- Responsible for maintaining consistency of all objects within the aggregate.
- External references to objects inside the aggregate go through the root.
When to use AggregateRoot: When an object manages a group of related objects and needs to enforce consistency rules across them.
Example:
pub struct TaskGraph {
id: String,
tasks: Vec<Task>,
edges: Vec<(String, String)>,
}
impl Entity for TaskGraph {
type Id = String;
fn id(&self) -> &Self::Id { &self.id }
}
impl AggregateRoot for TaskGraph {}A TaskGraph is an aggregate root because it manages a collection of Task entities and their dependency edges. Adding a task, removing a task, or modifying dependencies must go through the TaskGraph to ensure the graph remains valid (no cycles, no dangling references, proper ordering).
Note: the
TaskGraph/Tasksnippet above is a teaching example. NoTaskGraphtype exists in the current Aleph source — the actual domain aggregate root in the codebase isMemoryFact(src/memory/context/fact.rs), which implementspub trait AggregateRoot.
External code should never modify a Task directly — it should call methods on TaskGraph that maintain invariants:
impl TaskGraph {
pub fn add_task(&mut self, task: Task) -> Result<(), GraphError> {
// Validate no duplicate IDs
// Validate dependencies exist
// Maintain topological order
self.tasks.push(task);
Ok(())
}
pub fn update_status(&mut self, task_id: &str, status: TaskStatus) -> Result<(), GraphError> {
// Validate task exists
// Validate state transition is legal
// Propagate status changes to dependents
// ...
Ok(())
}
}ValueObject
A ValueObject is an immutable object defined entirely by its attributes. It has no identity — two value objects are equal if and only if all their attributes are equal.
pub trait ValueObject: Eq + Clone {}Characteristics:
- Immutable — once created, a value object never changes.
- No identity — equality is determined by comparing all attributes.
- Freely copyable and replaceable.
- Side-effect free — operations on value objects return new value objects.
When to use ValueObject: When an object represents a measurement, a classification, a status, or any concept where identity does not matter — only the value.
Example:
#[derive(Eq, PartialEq, Clone)]
pub struct TaskStatus {
pub state: TaskState,
pub progress: f32,
}
impl ValueObject for TaskStatus {}A TaskStatus is a value object because we only care about what the status is, not which specific instance it is. A status of "Running at 50%" is the same as any other status of "Running at 50%", regardless of when or where it was created.
Bounded Contexts
Aleph's domain is partitioned into bounded contexts — cohesive areas of the domain that have their own ubiquitous language, their own models, and their own rules. Objects in different contexts may share names but have different meanings.
teams/dispatcher Context
Responsibility: sub-task scheduling, parallel bucketing, Team coordination.
The teams/dispatcher context handles the execution side of the agent — TeamDispatcher decomposes complex requests into sub-tasks, while handoff.rs / runner.rs / schedule/{select,settle,reclaim,failure}.rs manage dependencies, timeouts, and failure recovery. The Act stage partitions work via src/tools/concurrency.rs::ConcurrencyClaim resource declarations (intra-bucket concurrent, inter-bucket serial), with no middleware doing intent judgment (R7/R10).
Important:
src/dispatcher/agent_types/{graph,task,status}.rsdoes not exist; noTaskGraphtype exists in the source tree. The table below lists the real types and locations most relevant to concurrent / task-recovery work.
| Type | DDD Role | Location |
|---|---|---|
ConcurrencyClaim | ValueObject (enum) | src/tools/concurrency.rs |
TaskStatus | ValueObject (enum) | src/a2a/domain/task.rs (A2A protocol domain, not the dispatcher core) |
TeamDispatcher | AggregateRoot | src/teams/dispatcher/{mod,handoff,runner}.rs |
The parallel-bucketing functions (batch_parallelizable / partition_parallel_groups) ensure:
- Calls within a bucket run concurrently.
- Buckets with conflicting resource declarations run serially.
claims_conflictbacks off when declarations overlap.
Memory Context
Responsibility: Fact storage, RAG retrieval, knowledge compression.
The Memory context manages the agent's persistent knowledge — storing facts, retrieving relevant context, and compressing old memories to maintain efficiency.
| Type | DDD Role | Location |
|---|---|---|
MemoryFact | AggregateRoot | src/memory/context/fact.rs |
ContextAnchor | ValueObject | src/memory/context/mod.rs |
NoteType | ValueObject (enum) | src/memory/context/enums.rs |
Correction: the paths and type names above are the real locations in the source tree (
memory/contextis a directory, not a file; the type isNoteType, notFactType).MemoryFact'simpl Entityandimpl AggregateRootlive insrc/memory/context/fact.rs.
The MemoryFact aggregate root ensures that:
- Facts are properly anchored to their context (conversation, topic, domain), expressed via
ContextAnchor. - Fact types are consistent and valid, gated by the
NoteTypeenum. - Compression operations preserve essential information (
src/memory/compression/+session_compactor/).
Intent Context (does not exist)
No standalone "intent detection" bounded context exists in the Aleph source:
src/intent/does not exist, andAggregatedIntent/IntentSignalreturn zero matches across the source tree. R7 explicitly lists "intent-detection rule engines" as usurping the LLM — intent classification is covered naturally by a single LLM inference call (R7 + R9). The only related runtime-config shell,src/config/types/smart_flow.rs::IntentDetectionConfig, has fallen intoINERT_SECTIONSpersrc/config/reload_impact.rsand no longer drives runtime classification.
POE Context (Retired)
The POE (Principle-Operation-Evaluation) runtime architecture was retired in 26.5+. Its "intelligence" migrated to system prompt templates (R9); success definition, validation, and evaluation are now naturally covered by a single LLM call. The Harness does not judge completion (R10's "Don't #3"), nor does it participate in evaluation. R10 removed the historical
DiminishingReturnsDetectorhard stop (note insrc/harness/tests/budget.rs:280) because it was a deterministic completion judgment inside the loop.The database layer still carries
-- POE Event Sourcing / Trust Scores / Contractsannotations insrc/resilience/database/state_database/schema.rs; they remain as event-sourcing audit scaffolding (the runtime intelligence itself has moved to the prompt).
Historical responsibility (archaeology only): success contracts, validation rules, evaluation results.
Context Relationships
The bounded contexts interact through well-defined interfaces, not direct dependencies:
┌────────────────┐
│ Thinker / │
│ PromptLayer │
│ (Evaluation │
│ via prompt) │
└───────┬────────┘
│ covered by a single LLM inference call
v
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ Harness │<──│ teams/ │──>│ Memory Context│
│ (Think→Act) │ │ dispatcher │ │ (Knowledge) │
│ R10 dumb loop│ │ (Execution) │ │ │
└────────────────┘ └────────────────┘ └────────────────┘
│ ^
│ dreaming feedback │
└─────────────────────────────────────────┘- Harness ↔ teams/dispatcher: the Act stage of Harness calls dispatcher; dispatcher returns the bucketed parallel-result back to Harness. Both are constrained by R10's "Dumb Loop" and never classify intent.
- teams/dispatcher ↔ Memory: dispatcher queries Memory for relevant context during execution (
src/memory/retrieval.rs). - Harness → Thinker / PromptLayer: the Think stage of the Think→Act loop calls the Thinker; the verdict is the model's natural output, not middleware output (R10's "Don't #5").
- Harness / Memory → dreaming: every successful turn enters the dreaming pipeline (
src/memory/dreaming/), which promotes skills under SkillOpt discipline; the promoted skills enrich Memory facts and give the next Think call denser context.
Each context owns its models and does not import types from other contexts. Communication happens through shared interfaces (traits, message types) defined at the boundary.
Implementing New Domain Types
When adding a new concept to Aleph's domain model, follow this decision process:
Step 1: Determine the Role
Ask these questions:
- Does this object need a unique identity? Does it need to be tracked, referenced, or have a lifecycle?
- Yes --> It is an Entity.
- Does it manage a group of related objects with consistency requirements?
- Yes --> It is an AggregateRoot (which is also an Entity).
- Is it just a container of attributes where identity does not matter?
- Yes --> It is a ValueObject.
Step 2: Implement the Trait
use crate::domain::{Entity, AggregateRoot, ValueObject};
// Entity
impl Entity for MyEntity {
type Id = String;
fn id(&self) -> &Self::Id { &self.id }
}
// AggregateRoot (also implement Entity first)
impl AggregateRoot for MyAggregate {}
// ValueObject (derive Eq and Clone)
#[derive(Eq, PartialEq, Clone)]
pub struct MyValue { /* fields */ }
impl ValueObject for MyValue {}Step 3: Place in the Right Context
Determine which bounded context the new type belongs to. If it does not fit any existing context, consider whether a new context is warranted.
Step 4: Add Tests
Domain invariants are expressed as plain Rust tests — invariants co-locate with the aggregate root inside #[cfg(test)] mod tests; cross-module behavior checks live in top-level tests/<name>.rs (they must be top-level — cargo auto-discovers only tests/*.rs; subdirectories require an explicit mod from a target to be compiled).
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn memory_fact_keeps_identity_after_validity_change() {
let fact = MemoryFact::new("user".into(), NoteType::Preference, vec![])
.with_valid_from(1000);
assert_eq!(fact.id(), "user");
}
}Where invariant and identity tests live today (examples):
src/memory/context/tests/fact_tests.rs—MemoryFactidentity stability, validity-window edges, aggregate-root invariantssrc/memory/context/tests/enum_tests.rs—NoteTypeenum-derivedFromStrconsistencysrc/domain/mod.rs::tests— minimal samples of theEntity/AggregateRoot/ValueObjecttraits
Design Principles
Keep Aggregates Small
Large aggregates create performance bottlenecks and contention. Each aggregate should contain only the objects that must be consistent with each other at all times. If two objects can be temporarily inconsistent, they belong in separate aggregates.
Prefer Value Objects
Value objects are simpler, safer, and more efficient than entities. If an object does not need identity or a lifecycle, make it a value object. This eliminates an entire class of bugs related to identity confusion and reference management.
Enforce Invariants at the Boundary
Aggregate roots are responsible for enforcing all business rules within their boundary. External code should never be able to put an aggregate into an invalid state. This means:
- Constructors validate initial state.
- Mutation methods validate state transitions.
- The type system prevents illegal operations where possible.
Use Rust's Type System
Rust's ownership model, trait system, and type-level programming align naturally with DDD:
- Ownership enforces aggregate boundaries — if the aggregate root owns its children, external code cannot hold mutable references to them.
- Traits express domain contracts without runtime cost.
- Enums model discriminated unions (like
NoteTypeorTaskState) with exhaustive matching. - Lifetimes prevent dangling references across context boundaries.
Further Reading
- Design Philosophy — The foundational principles that DDD serves
- Agent Thinking Model — Thinker / PromptLayer and the Harness Think→Act loop
- Five Layers of Emergence — How bounded contexts map to the L1-L5 model
- Architecture — Technical system architecture
Agent Thinking Model
How Aleph's agents observe, think, act, and learn — the Think→Act loop, the thin-harness/dumb-loop philosophy, dual-process cognition (System 1 + System 2), and first-principles anchoring.
Architectural Redlines
The ten non-negotiable design constraints that govern every development decision in Aleph — the highest-priority rules no code may violate.