Upgrading Timefold without breaking PowerSolver
PowerSolver's value is its dynamic model: a JSON problem (collections, variables, rules from a catalogue) becomes, at run time, a Timefold domain — classes generated with ByteBuddy (generation/*), constraints built from the rule catalogue (constraint/ConstraintBuilder), search settings applied to a SolverConfig (service/SearchSettings). Timefold does not offer this; it expects hand-written Java classes.
That is also why an upgrade needs care: the dynamic model relies on Timefold behaving the same way for classes it has never seen at compile time. This document says exactly what PowerSolver depends on, how to upgrade, and what must pass before an upgrade ships.
Current pins (powersolver-api/pom.xml): timefold.version = 2.0.0, bytebuddy.version = 1.15.11. Never use version ranges; every upgrade is a deliberate change of these two lines.
1. What PowerSolver uses from Timefold — four levels of risk
| Level | Timefold packages | Used in | Stability promise | Risk on upgrade |
|---|---|---|---|---|
| 1. Domain annotations | api.domain.* (@PlanningSolution, @PlanningEntity, @PlanningVariable, @PlanningListVariable, @PlanningPin, @PlanningId, @ValueRangeProvider, shadow variables incl. @ShadowVariable / @ShadowSources, @InverseRelationShadowVariable, @PreviousElementShadowVariable, @NextElementShadowVariable, @IndexShadowVariable) | generation/* — put on generated classes | public API, stable within a major version | low for minors; high for majors (the shadow-variable API has already changed across versions; PowerSolver uses the @ShadowSources form) |
| 2. Score & constraint streams | api.score.*, api.score.stream.* (ConstraintFactory, ConstraintCollectors, flattenLast, groupBy, HardSoftScore) | constraint/* | public API, stable | low |
| 3. Solver configuration | config.* (phases, construction heuristics, local search types, acceptors, move selectors, MoveIteratorFactoryConfig, termination) and api.solver.* | service/SearchSettings, service/SolverService | public, but defaults and accepted combinations evolve | medium: a new default can change plans |
| 4. Preview and internal APIs | preview.api.move.*, preview.api.domain.metamodel.* (preview), and impl.* (ScoreDirector, InnerScoreDirector, SolutionDescriptor, EntityDescriptor, GenuineVariableDescriptor, MoveIteratorFactory) | only service/GroupChangeMoveFactory (group moves for same_value_in_group) | none — may change in any minor or patch release | high |
Level 4 is contained in one file on purpose. Keep it that way: any new use of impl.* or preview.* must go into a small, isolated class like GroupChangeMoveFactory, never spread across the generator or the constraint builder. (Suggested guard: a unit test that fails when a class outside an allow-list imports ai.timefold.*.impl.* or ai.timefold.*.preview.*.)
ByteBuddy is the second dependency of the dynamic model: it must generate classes for the Java version the server runs. Upgrade ByteBuddy whenever the JDK is upgraded, independently of Timefold.
2. What protects the dynamic model during an upgrade
PowerSolver already has three independent safety nets. An upgrade is shippable only when all of them pass.
| Gate | What it proves | How to run |
|---|---|---|
| A. Engine test suite (≈ 490 tests) | the generator builds valid domains for every shape (basic, list, mixed, shadows, pins), every rule builds and counts as documented, the validator, the search settings, the HTTP contract | cd powersolver-api && mvn test |
| B. Differential quality gate | for every shipped problem (templates, demos, test cases), the engine's score and rule-by-rule breakdown equal those of the independent Python checker (powersolver-verify), which does not use Timefold at all | mvn test -Dtest=DifferentialExportTest -Ddifferential=true, then cd ../powersolver-verify && python -I -m pytest -q |
| C. Realistic benchmark (to build — see §4) | on realistic problems, the new version still reaches a valid plan within the time budget, with a comparable score | run every demo with its own settings; compare with the recorded baseline |
Gate B is the key one for the dynamic model: it does not trust Timefold's own score. If an upgrade changes how a generated domain or a constraint stream behaves, the engine's breakdown stops matching the checker and the gate fails, before any customer sees a wrong plan.
What the gates do not promise: identical plans. A new Timefold version may search differently; with a time limit the plan changes. That is expected. Gate C checks validity and quality bands, not exact assignments.
3. Upgrade procedure
3.1 Patch releases (2.0.x → 2.0.y): monthly or when a fix matters
- Read the release notes (github.com/TimefoldAI/timefold-solver/releases): bug fixes, anything under
implorpreviewtouched by the move or metamodel APIs. - Change
timefold.version, build, run gates A and B. - Run gate C on at least the largest demo.
- Deploy to the server; keep the previous image tagged for an immediate rollback (§3.4).
3.2 Minor releases (2.0 → 2.1): every few months
As a patch, plus:
- Compile with deprecation warnings (
mvn -Dmaven.compiler.showDeprecation=true compile) and remove every new deprecation PowerSolver uses: deprecated APIs disappear at the next major. - Check
GroupChangeMoveFactoryagainst the newMove/MutableSolutionView/ metamodel signatures (preview APIs change in minors). If it no longer compiles, it is the only file to adapt. - Check whether a new default changed (construction heuristic, local-search acceptor, environment mode, shadow-variable handling): PowerSolver keeps Timefold's defaults when a problem gives no search settings.
- Run gate C on every demo and compare the time-to-valid-plan and the final score with the baseline.
3.3 Major releases (2.x → 3.0): planned project, not a routine upgrade
- Read the upgrade recipe; Timefold publishes OpenRewrite migration recipes — apply them on a branch.
- Expect changes in level 1 (annotations, shadow variables) and level 4: plan time to adapt the generators (
ElementGenerator,HostGenerator,SolutionGenerator,DerivedShadowGenerator,ValuePoolGenerator) andGroupChangeMoveFactory. - All gates, plus a full run of the wizard and the PowerOrchestrator pipelines on a test server before production.
- Ship only when gate B is identical and gate C is at least as good.
3.4 Rollback
The engine is one container. Before deploying an upgrade, tag the running image (e.g. powersolver-api:2.0.0-ok); if anything is wrong after deployment, redeploy that tag. Problems and solutions are plain JSON, so nothing stored needs migrating either way.
4. What to build next to make upgrades routine
- Gate C — a realistic benchmark (does not exist yet). Use the demos (
powersolver-demos/*/problem*.json): record per problem the time to a valid plan, the final score and the hard part; on each upgrade, fail when a problem that was valid is no longer valid, or when time-to-valid grows by more than an agreed margin. Timefold's own benchmark module is already a dependency (timefold-solver-benchmark) and can produce the report. - The import guard for levels 1-4 described in §1.
- A fixed-seed run of the benchmark (
maxStepsinstead of a time limit) so that results are comparable between machines.
5. Summary
- The dynamic model is PowerSolver's own; Timefold is the search engine underneath. The contract with customers — the JSON problem, the rule catalogue, the independent checker — does not depend on Timefold.
- Pin versions; upgrade deliberately; patch often, minor regularly, major as a project.
- Keep internal/preview Timefold APIs in one isolated file.
- Ship an upgrade only when the engine tests, the independent-checker gate and the realistic benchmark all pass.