Upgrade Timefold Solver to the latest version
As Timefold Solver 2.x continues developing, we occasionally have to bring changes to our APIs. We will not break backwards compatibility during the lifetime of Timefold Solver 2.x, and that means your current code will not break. But we still want you to upgrade to the new APIs easily and quickly. We provide this upgrade recipe for users who choose to keep up to date with the latest and greatest in the solver space.
Many of the upgrade steps can be applied automatically using our migration tooling, but some manual changes may still be required. This upgrade recipe is intended to help you navigate those changes and upgrade your codebase with confidence.
1. Before you start
Throughout this document, we assume that you are upgrading from the latest available version of the 1.x line. Make sure to first upgrade to the 2.x version and only then follow this recipe.
Please refer to upgrade recipe for Timefold Solver 1.x or even upgrade recipe for OptaPlanner if you go way back.
2. Automatic upgrade to latest Timefold Solver
For many of the upgrade steps mentioned later, we provide a migration tool that can automatically apply those changes to Java files and Maven POMs. This tool is based on OpenRewrite and can be run as a Maven or Gradle plugin. To run the tool, execute the following command in your project directory:
-
Maven
-
Gradle
mvn org.openrewrite.maven:rewrite-maven-plugin:6.46.1:run -Drewrite.recipeArtifactCoordinates=ai.timefold.solver:timefold-solver-migration:SNAPSHOT -Drewrite.activeRecipes=ai.timefold.solver.migration.ToLatest
curl https://raw.githubusercontent.com/TimefoldAI/timefold-solver/refs/tags/vSNAPSHOT/tools/migration/upgrade-timefold.gradle > upgrade-timefold.gradle ; gradle -Dorg.gradle.jvmargs=-Xmx2G --init-script upgrade-timefold.gradle rewriteRun -DtimefoldSolverVersion=SNAPSHOT ; rm upgrade-timefold.gradle
| Our automatic migrations will not change the version of your frameworks. If you run into compatibility issues, please consult the integration guides for Spring or Quarkus. |
| The migration tool only works on JDK 25 or earlier, because OpenRewrite only supports LTS versions of Java. |
Having done that, you can check the local changes and commit them.
For the time being, Kotlin users need to follow the upgrade recipe and apply the steps manually.
3. Manual upgrade recipe
In addition to the automated changes listed above, there are some changes that require manual intervention. Every upgrade note indicates how likely your code will be affected by that change:
-
Major: Likely to affect your code.
-
Minor: Less likely to affect your code, especially if you’ve been upgrading Timefold Solver regularly, paying attention to new deprecations.
-
Recommended: We think this won’t affect you, but we’re listing this just to be safe.
The upgrade recipe often lists the changes as they apply to Java code. We kindly ask Kotlin users to translate the changes accordingly.
3.1. Conceptual changes
These changes are most likely to require you to change your code in a non-mechanical way, and they can only be automated partially, if at all.
Deprecation of @ShadowVariableInconsistent
Details
The solver can optionally increase the performance of shadow variable processing on dense variable graphs, in exchange for the user removing the @ShadowVariableInconsistent from their domain model. See the detailed migration guide.
Custom moves must declare exactly the list variable range they change
Details
A custom move that changes a list variable must bracket that change with beforeListVariableChanged(entity, variableName, fromIndex, toIndex) and a matching afterListVariableChanged(…).
The solver now verifies each such pair, and throws IllegalArgumentException if:
-
a second
beforeListVariableChangedopens for an entity whose previous one has not been closed yet, -
an
afterListVariableChangedhas no matchingbeforeListVariableChanged, or one arrives twice, -
the two calls disagree on
fromIndex, -
the reported range does not account for every element the move added or removed.
Previously the solver accepted these and silently corrupted the solution when the move was undone.
If your move now fails, make its range cover everything it changes.
For example, appending one element to the end of a list is beforeListVariableChanged(entity, "valueList", size, size) followed by afterListVariableChanged(entity, "valueList", size, size + 1).
New methods on Solver, SolverJob and FinalBestSolutionEvent
Details
To give the statistics of a finished run, these interfaces have new methods:
-
Solverhas the abstract methodsolveAndSummarize(problem).solve(problem)is now a default method that calls it. -
SolverJobhas the abstract methodgetResult(). -
FinalBestSolutionEventnow extendsSolverRunSummary.
Timefold Solver implements these interfaces, and we do not expect users to implement them.
Therefore, we do not expect you to have to change your code,
which is the only reason why we decided that this change to public API is safe.
If you implement one of these interfaces anyway, for example as a test double,
you must implement the new API.
For Solver, implement solveAndSummarize(problem) instead of solve(problem).
Simulated annealing cools with every termination
Details
Simulated annealing cools according to the time gradient of the terminations.
Before, it did not cool when diminished returns was the only termination,
when diminished returns was configured at solver level,
or when a nested and/or composite had no termination with a time gradient;
diminished returns now estimates the time left according to its current time window,
and simulated annealing heats up again when that window restarts.
So the results of simulated annealing in such configurations change.
Without any termination that can estimate the time left,
for example when only terminateEarly() stops the solver,
simulated annealing keeps its starting temperature, as before, and logs a warning.
Solver termination with AND waits for terminations that can only end a phase
Details
A solver termination that combines terminations with AND
now also waits for the terminations which can only end a phase:
diminished returns,
stepCountLimit and unimprovedStepCountLimit.
Each of them measures the current phase, and every other termination measures the whole solver.
Before, the solver ignored them and stopped as soon as the other terminations were met.
An AND of only such terminations stopped the solver before the first phase.
For example, with this solver termination, Local Search now runs for at least 5 minutes from the start of the solver, and also until diminished returns is met:
<solver>
<termination>
<terminationCompositionStyle>AND</terminationCompositionStyle>
<minutesSpentLimit>5</minutesSpentLimit>
<diminishedReturns/>
</termination>
...
</solver>
Before, the solver stopped after 5 minutes. To keep that behavior, remove the terminations which can only end a phase:
<solver>
<termination>
<minutesSpentLimit>5</minutesSpentLimit>
</termination>
...
</solver>
4. Changes to preview features
The API of preview features is not yet entirely stable, and changes based on user feedback can be expected. This section lists all such changes, so that users of preview features can adapt. All feedback is much appreciated!
4.1. Timefold Solver 2.7.0 hides move implementations
The concrete Move implementations of the Neighborhoods API (such as ChangeMove and ListChangeMove) are no longer public.
Build moves through the Moves factory class instead;
see built-in moves for the replacement.