# Timefold Solver Timefold Solver is an AI constraint solver for Java and Kotlin that optimizes planning problems such as Vehicle Routing, Employee Rostering, Maintenance Scheduling, Task Assignment, School Timetabling, Cloud Optimization, Conference Scheduling, and Job Shop Scheduling. It provides powerful and scalable optimization algorithms that can be embedded into any Java application to automatically find optimal or near-optimal solutions to complex planning and scheduling problems. Developed by the original OptaPlanner team, Timefold Solver is a lightweight, embeddable planning engine that implements advanced algorithms including construction heuristics, local search, and metaheuristics. It supports multiple frameworks including Spring Boot and Quarkus, offers both synchronous and asynchronous solving modes, and provides flexible score calculation through Constraint Streams, Easy Score Calculator, or Incremental Score Calculator approaches. The project includes modules for core solving, benchmarking, persistence (JSON/XML), and native framework integrations. ## SolverFactory - Create solver instances Creates Solver instances from XML configuration files or programmatic SolverConfig objects. Most applications only need one SolverFactory, which can be reused to build multiple Solver instances. ```java // Create from XML resource on classpath SolverFactory solverFactory = SolverFactory.createFromXmlResource("solverConfig.xml"); // Create from programmatic configuration SolverConfig solverConfig = new SolverConfig() .withSolutionClass(VehicleRoutingSolution.class) .withEntityClasses(Vehicle.class) .withConstraintProviderClass(VehicleRoutingConstraintProvider.class) .withTerminationSpentLimit(Duration.ofMinutes(5)); SolverFactory factory = SolverFactory.create(solverConfig); // Build and use solver Solver solver = factory.buildSolver(); VehicleRoutingSolution solution = solver.solve(problem); System.out.println("Final score: " + solution.getScore()); ``` ## Solver - Synchronous problem solving Solves planning problems synchronously by finding the best solution within the configured termination limits. The solve() method blocks until termination criteria are met or early termination is requested. ```java // Create solver from factory SolverFactory solverFactory = SolverFactory.createFromXmlResource("solverConfig.xml"); Solver solver = solverFactory.buildSolver(); // Create uninitialized problem VehicleRoutingSolution problem = new VehicleRoutingSolution(); problem.setVehicles(Arrays.asList(new Vehicle(1, 100), new Vehicle(2, 150))); problem.setCustomers(Arrays.asList( new Customer(1, 10, new Location(10, 20)), new Customer(2, 15, new Location(30, 40)) )); // Solve the problem (blocks until termination) VehicleRoutingSolution solution = solver.solve(problem); // Check results System.out.println("Score: " + solution.getScore()); for (Vehicle vehicle : solution.getVehicles()) { System.out.println("Vehicle " + vehicle.getId() + " customers: " + vehicle.getCustomers()); } // Terminate early from another thread if needed new Thread(() -> { try { Thread.sleep(60000); // Wait 1 minute solver.terminateEarly(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }).start(); ``` ## SolverManager - Asynchronous problem solving Manages multiple solving jobs asynchronously without blocking threads. Ideal for web applications and scenarios requiring concurrent solving of multiple problems. ```java // Create SolverManager SolverConfig solverConfig = new SolverConfig() .withSolutionClass(VehicleRoutingSolution.class) .withEntityClasses(Vehicle.class) .withConstraintProviderClass(VehicleRoutingConstraintProvider.class) .withTerminationSpentLimit(Duration.ofMinutes(2)); SolverManager solverManager = SolverManager.create(solverConfig); // Solve and listen for intermediate solutions VehicleRoutingSolution problem = createProblem(); SolverJob job = solverManager.solveAndListen( 1L, // Problem ID problem, bestSolution -> { // Called multiple times with improving solutions System.out.println("New best score: " + bestSolution.getScore()); saveToDatabase(bestSolution); }, finalBestSolution -> { // Called once when solving completes System.out.println("Final solution: " + finalBestSolution.getScore()); notifyUser(finalBestSolution); } ); // Get final solution asynchronously job.getFinalBestSolution().thenAccept(solution -> { System.out.println("Solving completed with score: " + solution.getScore()); }); // Terminate early if needed solverManager.terminateEarly(1L); // Close manager when application shuts down solverManager.close(); ``` ## @PlanningSolution - Define solution class Marks the main planning solution class that contains all planning entities, problem facts, and the score. This is the root object passed to the Solver. ```java @PlanningSolution public class VehicleRoutingSolution { @PlanningEntityCollectionProperty private List vehicles; @ProblemFactCollectionProperty private List customers; @PlanningScore private HardSoftScore score; @ConstraintWeightOverrides private Map> constraintWeights; // Constructors, getters, setters public VehicleRoutingSolution() {} public VehicleRoutingSolution(List vehicles, List customers) { this.vehicles = vehicles; this.customers = customers; } public List getVehicles() { return vehicles; } public void setVehicles(List vehicles) { this.vehicles = vehicles; } public List getCustomers() { return customers; } public void setCustomers(List customers) { this.customers = customers; } public HardSoftScore getScore() { return score; } public void setScore(HardSoftScore score) { this.score = score; } } ``` ## @PlanningEntity and @PlanningVariable - Define planning variables Marks classes and fields that the Solver can modify during optimization. Planning variables represent the decision variables that will be assigned values. ```java @PlanningEntity public class ProcessAssignment { private Process process; // Problem fact (immutable) @PlanningVariable(valueRangeProviderRefs = "computerRange") private Computer computer; // Planning variable (mutable) public ProcessAssignment() {} public ProcessAssignment(Process process) { this.process = process; } public Process getProcess() { return process; } public void setProcess(Process process) { this.process = process; } public Computer getComputer() { return computer; } public void setComputer(Computer computer) { this.computer = computer; } } @PlanningSolution public class CloudBalancingSolution { @ValueRangeProvider(id = "computerRange") @ProblemFactCollectionProperty private List computers; @PlanningEntityCollectionProperty private List assignments; @PlanningScore private HardSoftScore score; // Getters and setters } ``` ## @PlanningListVariable - Define ordered list variables Marks list fields where both the elements and their order are planning variables. Ideal for vehicle routing and similar problems where sequence matters. ```java @PlanningEntity public class Vehicle { private int capacity; private Location depot; @PlanningListVariable(valueRangeProviderRefs = "customerRange") private List customers; @PlanningPin private boolean pinned = false; public Vehicle() {} public Vehicle(int capacity, Location depot) { this.capacity = capacity; this.depot = depot; this.customers = new ArrayList<>(); } public int getCapacity() { return capacity; } public Location getDepot() { return depot; } public List getCustomers() { return customers; } public void setCustomers(List customers) { this.customers = customers; } public int getTotalDemand() { return customers.stream().mapToInt(Customer::getDemand).sum(); } } @PlanningSolution public class VehicleRoutingSolution { @ValueRangeProvider(id = "customerRange") @ProblemFactCollectionProperty private List customers; @PlanningEntityCollectionProperty private List vehicles; @PlanningScore private HardSoftScore score; // Getters and setters } ``` ## ConstraintProvider - Define scoring constraints Implements constraint-based scoring using the Constraint Streams API. Each constraint penalizes or rewards solutions based on business rules. ```java public class VehicleRoutingConstraintProvider implements ConstraintProvider { @Override public Constraint[] defineConstraints(ConstraintFactory constraintFactory) { return new Constraint[] { vehicleCapacity(constraintFactory), minimizeDistance(constraintFactory), minimizeVehicles(constraintFactory) }; } // Hard constraint: vehicle capacity must not be exceeded Constraint vehicleCapacity(ConstraintFactory factory) { return factory.forEach(Vehicle.class) .filter(vehicle -> vehicle.getTotalDemand() > vehicle.getCapacity()) .penalize(HardSoftScore.ONE_HARD, vehicle -> vehicle.getTotalDemand() - vehicle.getCapacity()) .asConstraint("Vehicle capacity"); } // Soft constraint: minimize total distance Constraint minimizeDistance(ConstraintFactory factory) { return factory.forEach(Vehicle.class) .filter(vehicle -> !vehicle.getCustomers().isEmpty()) .penalize(HardSoftScore.ONE_SOFT, vehicle -> calculateTotalDistance(vehicle)) .asConstraint("Minimize distance"); } // Soft constraint: minimize number of vehicles used Constraint minimizeVehicles(ConstraintFactory factory) { return factory.forEach(Vehicle.class) .filter(vehicle -> !vehicle.getCustomers().isEmpty()) .penalize(HardSoftScore.ofSoft(1000)) .asConstraint("Minimize vehicles"); } private int calculateTotalDistance(Vehicle vehicle) { int totalDistance = 0; Location previousLocation = vehicle.getDepot(); for (Customer customer : vehicle.getCustomers()) { totalDistance += previousLocation.getDistanceTo(customer.getLocation()); previousLocation = customer.getLocation(); } totalDistance += previousLocation.getDistanceTo(vehicle.getDepot()); return totalDistance; } } ``` ## SolverConfig - Programmatic configuration Configures solver behavior including termination criteria, algorithm phases, and score calculation without requiring XML files. ```java // Basic configuration SolverConfig solverConfig = new SolverConfig() .withSolutionClass(VehicleRoutingSolution.class) .withEntityClasses(Vehicle.class) .withConstraintProviderClass(VehicleRoutingConstraintProvider.class) .withTerminationSpentLimit(Duration.ofMinutes(5)); // Advanced configuration with custom phases SolverConfig advancedConfig = new SolverConfig() .withSolutionClass(VehicleRoutingSolution.class) .withEntityClasses(Vehicle.class) .withConstraintProviderClass(VehicleRoutingConstraintProvider.class) .withTerminationConfig(new TerminationConfig() .withSpentLimit(Duration.ofMinutes(10)) .withUnimprovedSpentLimit(Duration.ofMinutes(2)) .withBestScoreLimit("0hard/*soft")) .withPhases( new ConstructionHeuristicPhaseConfig(), new LocalSearchPhaseConfig() .withTerminationConfig(new TerminationConfig() .withSecondsSpentLimit(300L)) ); // Configuration with move thread count for parallelization SolverConfig parallelConfig = new SolverConfig() .withSolutionClass(VehicleRoutingSolution.class) .withEntityClasses(Vehicle.class) .withConstraintProviderClass(VehicleRoutingConstraintProvider.class) .withTerminationSpentLimit(Duration.ofMinutes(5)) .withMoveThreadCount("4"); // Or "AUTO" for automatic detection // Create solver from config SolverFactory factory = SolverFactory.create(parallelConfig); Solver solver = factory.buildSolver(); ``` ## Score Types - Built-in scoring systems Provides different score types for various constraint priority levels. Hard constraints must be satisfied, while soft constraints are optimized. ```java // HardSoftScore: Most common score type // Format: "hardScore/softScore" (e.g., "-5hard/-300soft") HardSoftScore score1 = HardSoftScore.of(-2, -450); HardSoftScore score2 = HardSoftScore.ofHard(-1); HardSoftScore score3 = HardSoftScore.ofSoft(-200); HardSoftScore zero = HardSoftScore.ZERO; // Comparing scores if (score1.isFeasible()) { // true if hard score >= 0 System.out.println("Solution is feasible"); } HardSoftScore better = score1.compareTo(score2) > 0 ? score1 : score2; // HardMediumSoftScore: Three priority levels HardMediumSoftScore score4 = HardMediumSoftScore.of(-1, -20, -300); System.out.println(score4.toShortString()); // "-1hard/-20medium/-300soft" // SimpleScore: Single score level SimpleScore score5 = SimpleScore.of(-500); // BendableScore: Configurable number of levels BendableScore score6 = BendableScore.of( new int[]{-2, -1}, // 2 hard levels new int[]{-100, -50, -25} // 3 soft levels ); // Score arithmetic HardSoftScore sum = score1.add(HardSoftScore.ofSoft(-50)); HardSoftScore multiplied = score1.multiply(2.0); // Using in constraints public class MyConstraintProvider implements ConstraintProvider { @Override public Constraint[] defineConstraints(ConstraintFactory factory) { return new Constraint[] { factory.forEach(Task.class) .filter(task -> task.getEndTime() > task.getDeadline()) .penalize(HardSoftScore.ONE_HARD) .asConstraint("Task deadline"), factory.forEach(Employee.class) .reward(HardSoftScore.ONE_SOFT, employee -> employee.getSatisfactionScore()) .asConstraint("Employee satisfaction") }; } } ``` ## PlannerBenchmarkFactory - Performance testing Creates benchmark instances to compare different solver configurations and measure performance across multiple datasets. ```java // Create from XML configuration PlannerBenchmarkFactory benchmarkFactory = PlannerBenchmarkFactory.createFromXmlResource("benchmarkConfig.xml"); PlannerBenchmark benchmark = benchmarkFactory.buildPlannerBenchmark(); benchmark.benchmark(); // Programmatic benchmark configuration PlannerBenchmarkConfig config = new PlannerBenchmarkConfig(); config.setBenchmarkDirectory(new File("target/benchmark")); // Add solver configurations to compare SolverBenchmarkConfig solverBenchmark1 = new SolverBenchmarkConfig(); solverBenchmark1.setName("5 minute config"); solverBenchmark1.setSolverConfig(new SolverConfig() .withSolutionClass(VehicleRoutingSolution.class) .withEntityClasses(Vehicle.class) .withConstraintProviderClass(VehicleRoutingConstraintProvider.class) .withTerminationSpentLimit(Duration.ofMinutes(5))); SolverBenchmarkConfig solverBenchmark2 = new SolverBenchmarkConfig(); solverBenchmark2.setName("2 minute config"); solverBenchmark2.setSolverConfig(new SolverConfig() .withSolutionClass(VehicleRoutingSolution.class) .withEntityClasses(Vehicle.class) .withConstraintProviderClass(VehicleRoutingConstraintProvider.class) .withTerminationSpentLimit(Duration.ofMinutes(2))); config.setSolverBenchmarkConfigList(Arrays.asList(solverBenchmark1, solverBenchmark2)); // Add problem datasets ProblemBenchmarksConfig problemConfig = new ProblemBenchmarksConfig(); problemConfig.setInputSolutionFile(new File("data/problem1.xml")); config.setProblemBenchmarksConfig(problemConfig); // Run benchmark PlannerBenchmarkFactory factory = PlannerBenchmarkFactory.create(config); PlannerBenchmark plannerBenchmark = factory.buildPlannerBenchmark(); plannerBenchmark.benchmark(); // Results saved to target/benchmark/index.html ``` ## Spring Boot Integration Auto-configures Timefold Solver in Spring Boot applications with minimal setup. Automatically scans for annotated classes and creates solver beans. ```java // Application.properties configuration // timefold.solver.termination.spent-limit=5m // timefold.solver.termination.unimproved-spent-limit=1m // timefold.solver.termination.best-score-limit=0hard/0soft // timefold.solver.environment-mode=FAST_ASSERT // timefold.solver.move-thread-count=AUTO @RestController @RequestMapping("/vehicle-routing") public class VehicleRoutingController { @Autowired private SolverManager solverManager; @Autowired private SolutionManager solutionManager; private final Map solutions = new ConcurrentHashMap<>(); private final AtomicLong problemIdSequence = new AtomicLong(); @PostMapping("/solve") public ResponseEntity solve(@RequestBody VehicleRoutingSolution problem) { long problemId = problemIdSequence.incrementAndGet(); solverManager.solveAndListen( problemId, problem, bestSolution -> solutions.put(problemId, bestSolution), finalSolution -> { solutions.put(problemId, finalSolution); System.out.println("Solving completed: " + finalSolution.getScore()); } ); return ResponseEntity.ok(problemId); } @GetMapping("/solution/{problemId}") public ResponseEntity getSolution(@PathVariable Long problemId) { VehicleRoutingSolution solution = solutions.get(problemId); if (solution == null) { return ResponseEntity.notFound().build(); } return ResponseEntity.ok(solution); } @PostMapping("/analyze") public ResponseEntity> analyze( @RequestBody VehicleRoutingSolution solution) { ScoreAnalysis analysis = solutionManager.analyze(solution); return ResponseEntity.ok(analysis); } @DeleteMapping("/terminate/{problemId}") public ResponseEntity terminate(@PathVariable Long problemId) { solverManager.terminateEarly(problemId); return ResponseEntity.ok().build(); } } @SpringBootApplication public class VehicleRoutingApplication { public static void main(String[] args) { SpringApplication.run(VehicleRoutingApplication.class, args); } } ``` ## Advanced Constraint Streams Demonstrates advanced Constraint Streams patterns including joins, grouping, conditional logic, and performance optimization techniques. ```java public class AdvancedConstraintProvider implements ConstraintProvider { @Override public Constraint[] defineConstraints(ConstraintFactory factory) { return new Constraint[] { employeeConflict(factory), requiredSkills(factory), fairWorkload(factory), consecutiveShifts(factory) }; } // Join two different entity types Constraint employeeConflict(ConstraintFactory factory) { return factory.forEachUniquePair(Shift.class, Joiners.equal(Shift::getEmployee), Joiners.overlapping( Shift::getStartTime, Shift::getEndTime )) .penalize(HardSoftScore.ONE_HARD) .asConstraint("Employee conflict"); } // Filter with multiple conditions Constraint requiredSkills(ConstraintFactory factory) { return factory.forEach(Shift.class) .filter(shift -> shift.getEmployee() != null) .filter(shift -> !shift.getEmployee().hasSkill(shift.getRequiredSkill())) .penalize(HardSoftScore.ONE_HARD) .asConstraint("Required skills"); } // Group by and aggregate Constraint fairWorkload(ConstraintFactory factory) { return factory.forEach(Shift.class) .filter(shift -> shift.getEmployee() != null) .groupBy( Shift::getEmployee, ConstraintCollectors.sum(Shift::getDurationMinutes) ) .filter((employee, totalMinutes) -> totalMinutes > employee.getMaxMinutes()) .penalize(HardSoftScore.ONE_SOFT, (employee, totalMinutes) -> totalMinutes - employee.getMaxMinutes()) .asConstraint("Fair workload"); } // Conditional constraints with ifExists Constraint consecutiveShifts(ConstraintFactory factory) { return factory.forEach(Shift.class) .filter(shift -> shift.getEmployee() != null) .ifExists(Shift.class, Joiners.equal(Shift::getEmployee), Joiners.equal(shift -> shift.getDate().plusDays(1), Shift::getDate)) .reward(HardSoftScore.ONE_SOFT) .asConstraint("Consecutive shifts bonus"); } } ``` ## Summary Timefold Solver addresses planning optimization use cases across industries including logistics (vehicle routing, delivery scheduling), workforce management (employee rostering, shift planning), manufacturing (job shop scheduling, maintenance planning), education (school timetabling, exam scheduling), and cloud computing (resource allocation, task assignment). The solver handles problems with varying scales from small scheduling tasks to large enterprise-level optimizations with thousands of entities and complex constraints, automatically finding solutions that balance hard requirements with soft optimization goals. Integration patterns include standalone Java applications using direct Solver or SolverManager APIs, REST API backends with Spring Boot autoconfiguration for web-based planning applications, microservices with Quarkus for cloud-native deployments with native compilation support, batch processing for offline optimization jobs, and real-time planning with continuous problem updates through ProblemChange support. The solver supports multiple deployment modes from embedded lightweight libraries to distributed systems, with flexible score calculation approaches (Constraint Streams for maintainability, Incremental for performance) and comprehensive benchmarking tools for configuration tuning.