Gather the domain objects in a planning solution

A Timetable wraps all Timeslot, Room, and Lesson instances of a single dataset. Furthermore, because it contains all lessons, each with a specific planning variable state, it is a planning solution and it has a score:

  • If lessons are still unassigned, then it is an uninitialized solution, for example, a solution with the score -4init/0hard/0soft.

  • If it breaks hard constraints, then it is an infeasible solution, for example, a solution with the score -2hard/-3soft.

  • If it adheres to all hard constraints, then it is a feasible solution, for example, a solution with the score 0hard/-7soft.

  • Java

  • Kotlin

  • Python

Create the src/main/java/org/acme/schooltimetabling/domain/ class:

package org.acme.schooltimetabling.domain;

import java.util.List;

import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty;
import ai.timefold.solver.core.api.domain.solution.PlanningScore;
import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
import ai.timefold.solver.core.api.domain.solution.ProblemFactCollectionProperty;
import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider;
import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore;

public class Timetable {

    private List<Timeslot> timeslots;
    private List<Room> rooms;
    private List<Lesson> lessons;

    private HardSoftScore score;

    public Timetable() {

    public Timetable(List<Timeslot> timeslots, List<Room> rooms, List<Lesson> lessons) {
        this.timeslots = timeslots;
        this.rooms = rooms;
        this.lessons = lessons;

    public List<Timeslot> getTimeslots() {
        return timeslots;

    public List<Room> getRooms() {
        return rooms;

    public List<Lesson> getLessons() {
        return lessons;

    public HardSoftScore getScore() {
        return score;


Create the src/main/kotlin/org/acme/schooltimetabling/TimetableApp.kt class:

package org.acme.schooltimetabling.domain

import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty
import ai.timefold.solver.core.api.domain.solution.PlanningScore
import ai.timefold.solver.core.api.domain.solution.PlanningSolution
import ai.timefold.solver.core.api.domain.solution.ProblemFactCollectionProperty
import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider
import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore
import ai.timefold.solver.core.api.solver.SolverStatus

data class Timetable (
    val timeslots: List<Timeslot>,
    val rooms: List<Room>,
    val lessons: List<Lesson>,
    var score: HardSoftScore? = null) {

    // No-arg constructor required for Timefold
    constructor() : this(emptyList(), emptyList(), emptyList())


Create the Solution class in src/hello_world/

from timefold.solver.domain import (planning_solution, PlanningEntityCollectionProperty,
                                    ProblemFactCollectionProperty, ValueRangeProvider,
from timefold.solver.score import HardSoftScore
from dataclasses import dataclass, field
from typing import Annotated

class Timetable:
    id: str
    timeslots: Annotated[list[Timeslot],
    rooms: Annotated[list[Room],
    lessons: Annotated[list[Lesson],
    score: Annotated[HardSoftScore, PlanningScore] = field(default=None)

The Timetable class has an @PlanningSolution annotation, so Timefold Solver knows that this class contains all of the input and output data.

Specifically, these classes are the input of the problem:

  • The timeslots field with all time slots

    • This is a list of problem facts, because they do not change during solving.

  • The rooms field with all rooms

    • This is a list of problem facts, because they do not change during solving.

  • The lessons field with all lessons

    • This is a list of planning entities, because they change during solving.

    • Of each Lesson:

      • The values of the timeslot and room fields are typically still null, so unassigned. They are planning variables.

      • The other fields, such as subject, teacher and studentGroup, are filled in. These fields are problem properties.

However, this class is also the output of the solution:

  • The lessons field for which each Lesson instance has non-null timeslot and room fields after solving.

  • The score field that represents the quality of the output solution, for example, 0hard/-5soft.

The value range providers

The timeslots field is a value range provider. It holds the Timeslot instances which Timefold Solver can pick from to assign to the timeslot field of Lesson instances. The timeslots field has an @ValueRangeProvider annotation to connect the @PlanningVariable with the @ValueRangeProvider, by matching the type of the planning variable with the type returned by the value range provider.

Following the same logic, the rooms field also has an @ValueRangeProvider annotation.

The problem fact and planning entity properties

Furthermore, Timefold Solver needs to know which Lesson instances it can change as well as how to retrieve the Timeslot and Room instances used for score calculation by your TimetableConstraintProvider.

The timeslots and rooms fields have an @ProblemFactCollectionProperty annotation, so your TimetableConstraintProvider can select from those instances.

The lessons has an @PlanningEntityCollectionProperty annotation, so Timefold Solver can change them during solving and your TimetableConstraintProvider can select from those too.