Domain Modeling
Define your planning domain with DomainModel, classes, and fields
This section covers how to define your planning domain using SolverForge’s domain modeling API.
In This Section
- Domain Model - Build domain models with
DomainModel, DomainClass, and FieldDescriptor - Planning Annotations - Configure planning behavior with
PlanningAnnotation types - Field Types - Supported field types including primitives, objects, collections, and scores
Overview
A domain model describes the structure of your planning problem:
use solverforge_core::domain::{
DomainClass, DomainModel, FieldDescriptor, FieldType,
PlanningAnnotation, PrimitiveType, ScoreType,
};
let model = DomainModel::builder()
.add_class(
DomainClass::new("Shift")
.with_annotation(PlanningAnnotation::PlanningEntity)
.with_field(/* ... */)
)
.add_class(
DomainClass::new("Schedule")
.with_annotation(PlanningAnnotation::PlanningSolution)
.with_field(/* ... */)
)
.build();
Key Concepts
| Concept | Purpose | Annotation |
|---|
| Planning Entity | Object modified by solver | @PlanningEntity |
| Planning Variable | Field assigned by solver | @PlanningVariable |
| Planning Solution | Container for all data | @PlanningSolution |
| Problem Fact | Read-only data | (no annotation needed) |
| Value Range Provider | Source of variable values | @ValueRangeProvider |
Model Validation
Use build_validated() to catch configuration errors early:
let model = DomainModel::builder()
.add_class(/* ... */)
.build_validated()?; // Returns SolverForgeError on invalid model
Validation checks:
- Solution class exists with
@PlanningSolution - At least one entity class with
@PlanningEntity - Each entity has at least one
@PlanningVariable - Solution class has a
@PlanningScore field
1 - Domain Model
Build domain models with DomainModel, DomainClass, and FieldDescriptor
The DomainModel is the central data structure that describes your planning domain.
Building a Model
Use the builder pattern to construct a domain model:
use solverforge_core::domain::{
DomainClass, DomainModel, FieldDescriptor, FieldType,
PlanningAnnotation, PrimitiveType, ScoreType,
};
let model = DomainModel::builder()
.add_class(DomainClass::new("Employee")/* ... */)
.add_class(DomainClass::new("Shift")/* ... */)
.add_class(DomainClass::new("Schedule")/* ... */)
.build();
DomainClass
Each class in your domain is defined with DomainClass:
let shift = DomainClass::new("Shift")
.with_annotation(PlanningAnnotation::PlanningEntity)
.with_field(
FieldDescriptor::new("id", FieldType::Primitive(PrimitiveType::String))
.with_planning_annotation(PlanningAnnotation::PlanningId),
)
.with_field(
FieldDescriptor::new("employee", FieldType::object("Employee"))
.with_planning_annotation(
PlanningAnnotation::planning_variable(vec!["employees".to_string()])
),
)
.with_field(FieldDescriptor::new(
"start",
FieldType::Primitive(PrimitiveType::DateTime),
))
.with_field(FieldDescriptor::new(
"end",
FieldType::Primitive(PrimitiveType::DateTime),
));
Class Methods
| Method | Purpose |
|---|
DomainClass::new(name) | Create a new class |
.with_annotation(ann) | Add a class-level annotation |
.with_field(field) | Add a field |
.is_planning_entity() | Check if @PlanningEntity |
.is_planning_solution() | Check if @PlanningSolution |
.get_planning_variables() | Iterator over planning variable fields |
FieldDescriptor
Each field is defined with FieldDescriptor:
let employee_field = FieldDescriptor::new("employee", FieldType::object("Employee"))
.with_planning_annotation(PlanningAnnotation::planning_variable(vec!["employees"]))
.with_accessor(DomainAccessor::new("getEmployee", "setEmployee"));
Field Methods
| Method | Purpose |
|---|
FieldDescriptor::new(name, type) | Create a new field |
.with_planning_annotation(ann) | Add a planning annotation |
.with_shadow_annotation(ann) | Add a shadow variable annotation |
.with_accessor(acc) | Override default accessor names |
.is_planning_variable() | Check if @PlanningVariable |
Complete Example
let model = DomainModel::builder()
// Problem fact: Employee (not modified by solver)
.add_class(
DomainClass::new("Employee")
.with_field(
FieldDescriptor::new("name", FieldType::Primitive(PrimitiveType::String))
.with_planning_annotation(PlanningAnnotation::PlanningId),
)
.with_field(FieldDescriptor::new(
"skills",
FieldType::list(FieldType::Primitive(PrimitiveType::String)),
)),
)
// Planning entity: Shift (employee assignment decided by solver)
.add_class(
DomainClass::new("Shift")
.with_annotation(PlanningAnnotation::PlanningEntity)
.with_field(
FieldDescriptor::new("id", FieldType::Primitive(PrimitiveType::String))
.with_planning_annotation(PlanningAnnotation::PlanningId),
)
.with_field(
FieldDescriptor::new("employee", FieldType::object("Employee"))
.with_planning_annotation(
PlanningAnnotation::planning_variable(vec!["employees".to_string()])
),
)
.with_field(FieldDescriptor::new(
"requiredSkill",
FieldType::Primitive(PrimitiveType::String),
)),
)
// Planning solution: Schedule (container)
.add_class(
DomainClass::new("Schedule")
.with_annotation(PlanningAnnotation::PlanningSolution)
.with_field(
FieldDescriptor::new("employees", FieldType::list(FieldType::object("Employee")))
.with_planning_annotation(PlanningAnnotation::ProblemFactCollectionProperty)
.with_planning_annotation(PlanningAnnotation::value_range_provider("employees")),
)
.with_field(
FieldDescriptor::new("shifts", FieldType::list(FieldType::object("Shift")))
.with_planning_annotation(PlanningAnnotation::PlanningEntityCollectionProperty),
)
.with_field(
FieldDescriptor::new("score", FieldType::Score(ScoreType::HardSoft))
.with_planning_annotation(PlanningAnnotation::planning_score()),
),
)
.build_validated()?;
Converting to DTO
Convert the model to a DTO for the solver request:
let domain_dto = model.to_dto();
The to_dto() method:
- Generates accessor names matching WASM exports:
get_{Class}_{field}, set_{Class}_{field} - Adds setters for planning variables and collection properties
- Adds mapper for the solution class (
parseSchedule, scheduleString)
DomainModel Methods
| Method | Purpose |
|---|
DomainModel::builder() | Create a builder |
.add_class(class) | Add a class |
.build() | Build without validation |
.build_validated() | Build with validation |
model.get_class(name) | Get a class by name |
model.get_solution_class() | Get the solution class |
model.get_entity_classes() | Iterator over entity classes |
model.to_dto() | Convert to solver DTO |
model.validate() | Validate the model |
2 - Planning Annotations
Configure planning behavior with PlanningAnnotation types
Annotations configure how the solver interprets your domain model.
Class Annotations
PlanningEntity
Marks a class as a planning entity - an object whose planning variables are assigned by the solver.
DomainClass::new("Shift")
.with_annotation(PlanningAnnotation::PlanningEntity)
PlanningSolution
Marks a class as the planning solution - the container for all problem facts, entities, and the score.
DomainClass::new("Schedule")
.with_annotation(PlanningAnnotation::PlanningSolution)
Field Annotations
PlanningId
Marks a field as the unique identifier for instances of the class.
FieldDescriptor::new("id", FieldType::Primitive(PrimitiveType::String))
.with_planning_annotation(PlanningAnnotation::PlanningId)
PlanningVariable
Marks a field whose value is determined by the solver. Must reference value range providers.
// Basic planning variable
FieldDescriptor::new("employee", FieldType::object("Employee"))
.with_planning_annotation(
PlanningAnnotation::planning_variable(vec!["employees".to_string()])
)
// Planning variable that allows null (unassigned)
FieldDescriptor::new("room", FieldType::object("Room"))
.with_planning_annotation(
PlanningAnnotation::planning_variable_unassigned(vec!["rooms".to_string()])
)
Parameters:
value_range_provider_refs: List of value range provider IDs that supply valid valuesallows_unassigned: If true, the variable can remain unassigned (null)
PlanningListVariable
Marks a list field where the solver assigns which elements belong to the list.
FieldDescriptor::new("visits", FieldType::list(FieldType::object("Visit")))
.with_planning_annotation(
PlanningAnnotation::planning_list_variable(vec!["visits".to_string()])
)
PlanningScore
Marks the score field on the solution class.
// Standard score
FieldDescriptor::new("score", FieldType::Score(ScoreType::HardSoft))
.with_planning_annotation(PlanningAnnotation::planning_score())
// Bendable score with specific levels
FieldDescriptor::new("score", FieldType::Score(ScoreType::Bendable { hard_levels: 2, soft_levels: 3 }))
.with_planning_annotation(PlanningAnnotation::planning_score_bendable(2, 3))
ValueRangeProvider
Marks a field that provides valid values for planning variables.
FieldDescriptor::new("employees", FieldType::list(FieldType::object("Employee")))
.with_planning_annotation(PlanningAnnotation::value_range_provider("employees"))
The ID ("employees") must match the value_range_provider_refs in corresponding planning variables.
ProblemFactCollectionProperty
Marks a collection of problem facts (read-only data) on the solution class.
FieldDescriptor::new("employees", FieldType::list(FieldType::object("Employee")))
.with_planning_annotation(PlanningAnnotation::ProblemFactCollectionProperty)
PlanningEntityCollectionProperty
Marks a collection of planning entities on the solution class.
FieldDescriptor::new("shifts", FieldType::list(FieldType::object("Shift")))
.with_planning_annotation(PlanningAnnotation::PlanningEntityCollectionProperty)
PlanningPin
Marks a boolean field that pins an entity’s assignment (prevents the solver from changing it).
FieldDescriptor::new("pinned", FieldType::Primitive(PrimitiveType::Bool))
.with_planning_annotation(PlanningAnnotation::PlanningPin)
InverseRelationShadowVariable
Marks a shadow variable that tracks the inverse of another planning variable.
FieldDescriptor::new("vehicle", FieldType::object("Vehicle"))
.with_planning_annotation(PlanningAnnotation::inverse_relation_shadow("visits"))
Annotation Summary
| Annotation | Target | Purpose |
|---|
PlanningEntity | Class | Mark as planning entity |
PlanningSolution | Class | Mark as solution container |
PlanningId | Field | Unique identifier |
PlanningVariable | Field | Solver-assigned value |
PlanningListVariable | Field | Solver-assigned list |
PlanningScore | Field | Score field on solution |
ValueRangeProvider | Field | Source of valid values |
ProblemFactCollectionProperty | Field | Problem fact collection |
PlanningEntityCollectionProperty | Field | Entity collection |
PlanningPin | Field | Pin entity assignment |
InverseRelationShadowVariable | Field | Shadow inverse relation |
Helper Methods
use solverforge_core::domain::PlanningAnnotation;
// Create planning variable
PlanningAnnotation::planning_variable(vec!["rooms".to_string()])
PlanningAnnotation::planning_variable_unassigned(vec!["slots".to_string()])
// Create list variable
PlanningAnnotation::planning_list_variable(vec!["tasks".to_string()])
// Create score annotation
PlanningAnnotation::planning_score()
PlanningAnnotation::planning_score_bendable(2, 3)
// Create value range provider
PlanningAnnotation::value_range_provider("timeslots")
// Create shadow variable
PlanningAnnotation::inverse_relation_shadow("visits")
Multiple Annotations
A field can have multiple annotations:
FieldDescriptor::new("employees", FieldType::list(FieldType::object("Employee")))
.with_planning_annotation(PlanningAnnotation::ProblemFactCollectionProperty)
.with_planning_annotation(PlanningAnnotation::value_range_provider("employees"))
3 - Field Types
Supported field types including primitives, objects, collections, and scores
SolverForge supports various field types for domain modeling.
Primitive Types
Basic value types:
use solverforge_core::domain::{FieldType, PrimitiveType};
// Boolean
FieldType::Primitive(PrimitiveType::Bool)
// Integers
FieldType::Primitive(PrimitiveType::Int) // 32-bit
FieldType::Primitive(PrimitiveType::Long) // 64-bit
// Floating point
FieldType::Primitive(PrimitiveType::Float) // 32-bit
FieldType::Primitive(PrimitiveType::Double) // 64-bit
// String
FieldType::Primitive(PrimitiveType::String)
// Date/Time (stored as epoch values)
FieldType::Primitive(PrimitiveType::Date) // LocalDate - epoch day (i64)
FieldType::Primitive(PrimitiveType::DateTime) // LocalDateTime - epoch second (i64)
Date and Time Handling
Dates and times are stored as integers:
Date (LocalDate): Epoch day (days since 1970-01-01)DateTime (LocalDateTime): Epoch second (seconds since 1970-01-01T00:00:00)
// Field definition
FieldDescriptor::new("start", FieldType::Primitive(PrimitiveType::DateTime))
// JSON representation
// "start": "2025-01-15T08:00:00" is converted to epoch seconds
Object Types
References to other domain classes:
// Reference to Employee class
FieldType::object("Employee")
// Usage in field descriptor
FieldDescriptor::new("employee", FieldType::object("Employee"))
Collection Types
List
Ordered collection (most common):
// List of strings
FieldType::list(FieldType::Primitive(PrimitiveType::String))
// List of objects
FieldType::list(FieldType::object("Shift"))
Array
Fixed-size array (similar to List in behavior):
FieldType::array(FieldType::Primitive(PrimitiveType::Int))
Set
Unordered unique elements:
FieldType::set(FieldType::object("Skill"))
Map
Key-value mapping:
FieldType::map(
FieldType::Primitive(PrimitiveType::String), // Key type
FieldType::object("Employee") // Value type
)
Score Types
Score types for the planning solution:
use solverforge_core::domain::ScoreType;
// Single dimension
FieldType::Score(ScoreType::Simple)
FieldType::Score(ScoreType::SimpleDecimal)
// Two dimensions (hard/soft)
FieldType::Score(ScoreType::HardSoft)
FieldType::Score(ScoreType::HardSoftDecimal)
// Three dimensions (hard/medium/soft)
FieldType::Score(ScoreType::HardMediumSoft)
FieldType::Score(ScoreType::HardMediumSoftDecimal)
// Configurable dimensions
FieldType::Score(ScoreType::Bendable { hard_levels: 2, soft_levels: 3 })
FieldType::Score(ScoreType::BendableDecimal { hard_levels: 2, soft_levels: 3 })
Type String Conversion
FieldType::to_type_string() converts to Java-compatible type strings:
| Rust Type | Java Type String |
|---|
PrimitiveType::Bool | boolean |
PrimitiveType::Int | int |
PrimitiveType::Long | long |
PrimitiveType::Float | float |
PrimitiveType::Double | double |
PrimitiveType::String | String |
PrimitiveType::Date | LocalDate |
PrimitiveType::DateTime | LocalDateTime |
FieldType::object("Foo") | Foo |
FieldType::list(...) | ...[] |
ScoreType::HardSoft | HardSoftScore |
Examples
Employee Class
DomainClass::new("Employee")
.with_field(
FieldDescriptor::new("name", FieldType::Primitive(PrimitiveType::String))
.with_planning_annotation(PlanningAnnotation::PlanningId),
)
.with_field(FieldDescriptor::new(
"skills",
FieldType::list(FieldType::Primitive(PrimitiveType::String)),
))
.with_field(FieldDescriptor::new(
"unavailableDates",
FieldType::list(FieldType::Primitive(PrimitiveType::Date)),
))
Shift Class
DomainClass::new("Shift")
.with_annotation(PlanningAnnotation::PlanningEntity)
.with_field(
FieldDescriptor::new("id", FieldType::Primitive(PrimitiveType::String))
.with_planning_annotation(PlanningAnnotation::PlanningId),
)
.with_field(
FieldDescriptor::new("employee", FieldType::object("Employee"))
.with_planning_annotation(PlanningAnnotation::planning_variable(vec!["employees"])),
)
.with_field(FieldDescriptor::new(
"start",
FieldType::Primitive(PrimitiveType::DateTime),
))
.with_field(FieldDescriptor::new(
"end",
FieldType::Primitive(PrimitiveType::DateTime),
))
.with_field(FieldDescriptor::new(
"requiredSkill",
FieldType::Primitive(PrimitiveType::String),
))
Schedule Class
DomainClass::new("Schedule")
.with_annotation(PlanningAnnotation::PlanningSolution)
.with_field(
FieldDescriptor::new("employees", FieldType::list(FieldType::object("Employee")))
.with_planning_annotation(PlanningAnnotation::ProblemFactCollectionProperty)
.with_planning_annotation(PlanningAnnotation::value_range_provider("employees")),
)
.with_field(
FieldDescriptor::new("shifts", FieldType::list(FieldType::object("Shift")))
.with_planning_annotation(PlanningAnnotation::PlanningEntityCollectionProperty),
)
.with_field(
FieldDescriptor::new("score", FieldType::Score(ScoreType::HardSoft))
.with_planning_annotation(PlanningAnnotation::planning_score()),
)
Collection Checks
let list_type = FieldType::list(FieldType::object("Shift"));
assert!(list_type.is_collection()); // true
let object_type = FieldType::object("Employee");
assert!(!object_type.is_collection()); // false