Scheduling is a common and challenging problem in many industries. Whether you are planning employee shifts, vehicle routes, conference sessions, or manufacturing jobs, you often face a large number of possible assignments and complex rules that govern what is valid and desirable. Solving these problems manually or with simple heuristics can be time-consuming and error-prone.
Fortunately, there is a powerful tool that can help: Timefold Solver. Timefold Solver is a Java library that uses constraint programming and metaheuristic search to find good solutions to combinatorial optimization problems. It allows you to model your domain in plain Java, define your constraints in a readable way, and let the solver do the hard work of finding a schedule that satisfies your rules and preferences.
Timefold Solver is a general-purpose optimization engine that can be applied to a wide range of scheduling and planning problems. Some common use cases include:
- Employee shift scheduling and rostering
- Vehicle routing and field service planning
- Maintenance scheduling
- School timetabling and conference scheduling
- Task assignment and project job scheduling
- Manufacturing and job shop sequencing
Timefold Solver is open source and released under the Apache License 2.0. You can find it on GitHub. The open source edition is fully functional and includes the full constraint solver engine. So you will be able to model and solve real-world scheduling problems without any limitations.
Timefold also offers paid versions called Solver Plus and Solver Enterprise, which include additional features and support. You can learn more about it on the license page.
Setup ¶
Timefold Solver 2.0 requires Java 17 or higher.
To get started, you can create a new Java Maven project and add the Timefold Solver dependency. Importing the timefold-solver-bom aligns the versions of all Timefold artifacts. Here is an example pom.xml snippet:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>ai.timefold.solver</groupId>
<artifactId>timefold-solver-bom</artifactId>
<version>2.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependencies>
<dependency>
<groupId>ai.timefold.solver</groupId>
<artifactId>timefold-solver-core</artifactId>
</dependency>
Demo problem: scheduling a film festival ¶
Our fictional festival has three venues:
- Grand Theater: 220 seats, 4K projection, and a stage for Q&A sessions
- Warehouse Screen: 180 seats, 4K projection, no stage
- Rooftop Cinema: 110 seats, stage, no 4K support
Each screening has:
- an expected audience size
- a runtime in minutes
- optional 4K and stage requirements
- a premiere flag
- an audience segment
- a guest list
The festival runs from Thursday to Saturday, with multiple start windows each day. The goal is to assign each screening to a timeslot and a screen while respecting the following constraints:
- hard rule: two screenings cannot overlap on the same screen
- hard rule: guests cannot appear in overlapping screenings
- hard rule: 4K/stage requirements must be respected
- hard rule: some screens are only available in some start windows
- soft rule: premieres should prefer prime time
- soft rule: overlapping screenings for the same audience segment are undesirable
- soft rule: in an over-constrained schedule, some screenings may need to stay unassigned
Modeling the problem ¶
With Timefold, you can model your scheduling problem using plain Java classes. You will define your planning entities (e.g., Screening), your problem facts (e.g., Timeslot, Screen), and your planning solution (e.g., FilmFestivalSchedule). You will also annotate the relevant fields with Timefold annotations such as @PlanningEntity, @PlanningVariable, and @PlanningScore.
The Screening class will represent each film screening and will have planning variables for the assigned timeslot and screen. @PlanningEntity marks it as something that the solver will manipulate, and @PlanningId provides a unique identifier for each screening. Every planning entity must have a unique ID, which helps Timefold track them across iterations. The @PlanningVariable annotations indicate which fields the solver can change to find a solution. In this case, the timeslot and screen fields are the planning variables that the solver will assign values to. The allowsUnassigned = true attribute means that the solver can choose to leave a screening unassigned if it cannot find a feasible schedule for it. The valueRangeProviderRefs attribute points to the lists of available timeslots and screens defined in the planning solution. The name of the range provider must match the id of the corresponding @ValueRangeProvider in the solution class.
Timefold uses reflection to clone planning entities and the planning solution while it explores the search space, so both the Screening class and the FilmFestivalSchedule class need a public no-arg constructor.
@PlanningEntity
public class Screening {
@PlanningId
private String id;
private String title;
private String director;
private final Set<String> guests;
private String audienceSegment;
private int expectedAudience;
private int durationMinutes;
private boolean requires4k;
private boolean requiresStage;
private boolean premiere;
@PlanningVariable(valueRangeProviderRefs = "timeslotRange", allowsUnassigned = true)
private Timeslot timeslot;
@PlanningVariable(valueRangeProviderRefs = "screenRange", allowsUnassigned = true)
private Screen screen;
public Screening() {
this.guests = Set.of();
}
The solution class is annotated with @PlanningSolution and contains the lists of problem facts and planning entities. The @ProblemFactCollectionProperty annotation indicates that the timeslots and screens fields are collections of problem facts that the solver will use as input. The @ValueRangeProvider annotation defines the value ranges for the planning variables in the Screening entity. The @PlanningEntityCollectionProperty annotation indicates that the screenings field is a collection of planning entities that the solver will manipulate to find a solution. Finally, the @PlanningScore annotation indicates that the score field will hold the score of the solution, which is calculated based on the defined constraints. The score field needs a setter so that the solver can write the calculated score back into the solution. The solver tries to maximize the score by finding a schedule that satisfies all hard constraints and optimizes the soft constraints.
@PlanningSolution
public class FilmFestivalSchedule {
@ProblemFactCollectionProperty
@ValueRangeProvider(id = "timeslotRange")
private List<Timeslot> timeslots;
@ProblemFactCollectionProperty
@ValueRangeProvider(id = "screenRange")
private List<Screen> screens;
@PlanningEntityCollectionProperty
private List<Screening> screenings;
@PlanningScore
private HardSoftScore score;
public FilmFestivalSchedule() {
}
Defining the constraints ¶
The constraints are defined in a class that implements ConstraintProvider. You will implement the defineConstraints method to return a list of Constraint instances that represent the hard and soft rules of your scheduling problem.
public class FilmFestivalConstraintProvider implements ConstraintProvider {
@Override
public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
return new Constraint[]{partiallyAssigned(constraintFactory), screenConflict(constraintFactory),
guestConflict(constraintFactory), unavailableScreen(constraintFactory), unsupportedScreen(constraintFactory),
audienceOverflow(constraintFactory), unassignedScreening(constraintFactory),
premiereOutsidePrimeTime(constraintFactory), audienceSegmentClash(constraintFactory),
seatMismatch(constraintFactory)};
}
FilmFestivalConstraintProvider.java
Each constraint is defined using the Constraint Streams API, which allows you to express complex logic in a readable way. For example, to define the hard constraint that two screenings cannot overlap on the same screen, you can use the following code with ConstraintFactory and Joiners:
Constraint screenConflict(ConstraintFactory constraintFactory) {
return constraintFactory.forEachUniquePair(Screening.class, Joiners.equal(Screening::getScreen))
.filter((left, right) -> left.getScreen() != null && left.overlapsWith(right)).penalize(HardSoftScore.ONE_HARD)
.asConstraint("screen conflict");
}
FilmFestivalConstraintProvider.java
This constraint uses forEachUniquePair to iterate over all unique pairs of screenings, joins them on the condition that they are assigned to the same screen, filters out pairs that do not actually overlap in time, and penalizes any pair that violates this rule with a hard score penalty.
Another hard constraint is that guests cannot appear in overlapping screenings. This can be defined as follows:
Constraint guestConflict(ConstraintFactory constraintFactory) {
return constraintFactory.forEachUniquePair(Screening.class)
.filter((left, right) -> left.overlapsWith(right) && left.sharesGuestWith(right))
.penalize(HardSoftScore.ONE_HARD).asConstraint("guest conflict");
}
FilmFestivalConstraintProvider.java
This constraint also uses forEachUniquePair to iterate over all unique pairs of screenings, filters them to only consider pairs that overlap in time, and then checks if they share any guests. If they do, it penalizes that pair with a hard score penalty.
Soft constraints can be defined similarly, but they use reward or penalize with a soft score instead of a hard score. In this example, that score is represented by HardSoftScore. For example, to define the soft constraint that premieres should prefer prime time, you can use the following code:
Constraint premiereOutsidePrimeTime(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(Screening.class)
.filter(screening -> screening.isPremiere() && screening.isAssigned() && !screening.getTimeslot().isPrimeTime())
.penalize(HardSoftScore.ONE_SOFT, _ -> 20).asConstraint("premiere outside prime time");
}
FilmFestivalConstraintProvider.java
This constraint iterates over all screenings, filters them to only consider premieres that are assigned to a timeslot that is not prime time, and penalizes each of those screenings with a soft score penalty of 20. The number 20 is an arbitrary weight that you can adjust based on how important this preference is compared to other constraints.
Note the difference between forEach and forEachIncludingUnassigned. When a planning variable is declared with allowsUnassigned = true, forEach automatically skips entities whose planning variable is null, so most constraints can ignore the unassigned case. To reason about unassigned entities, for example to penalize screenings that the solver decided to leave out, you have to use forEachIncludingUnassigned, which yields every entity regardless of its assignment state. The unassignedScreening and partiallyAssigned constraints in this example use it for that reason.
Constraint unassignedScreening(ConstraintFactory constraintFactory) {
return constraintFactory.forEachIncludingUnassigned(Screening.class).filter(Screening::isUnassigned)
.penalize(HardSoftScore.ONE_SOFT, Screening::unassignedPenalty).asConstraint("unassigned screening");
}
FilmFestivalConstraintProvider.java
reward is not used in this example, but it works similarly to penalize except that it increases the score instead of decreasing it. You can use reward to express positive preferences, such as rewarding screenings that are scheduled in prime time or that have a large expected audience.
The names that are set with asConstraint are useful for debugging and score explanation, as they will appear in the score breakdown when you analyze the solution.
Running the solver ¶
With the model and constraints defined, you can now create a SolverFactory and run the solver to find a schedule. DemoData creates some sample data for timeslots, screens, and screenings.
The solver factory takes a SolverConfig that specifies the solution class, entity classes, constraint provider class, and termination conditions. In this example, we set a time limit of 2 seconds for the solver to find a solution by using TerminationConfig. For such a small problem, that is more than enough time to find a good solution. The termination condition can also be based on other criteria, such as a score threshold or a number of iterations.
public static FilmFestivalSchedule solveSampleFestival() {
FilmFestivalSchedule problem = DemoData.sampleFestival();
SolverFactory<FilmFestivalSchedule> solverFactory = SolverFactory
.create(new SolverConfig().withSolutionClass(FilmFestivalSchedule.class).withEntityClasses(Screening.class)
.withConstraintProviderClass(FilmFestivalConstraintProvider.class)
.withTerminationConfig(new TerminationConfig().withSpentLimit(Duration.ofSeconds(2))));
return solverFactory.buildSolver().solve(problem);
}
FilmFestivalScheduleSupport.java
solve is a blocking call that runs on the calling thread and returns the best solution found before the termination condition fires. That is fine for a command-line demo, but in a server application you usually do not want to block a request thread for seconds at a time. For that case Timefold provides SolverManager, which schedules solver runs on a background executor, lets you submit problems by job id, and exposes hooks for streaming intermediate best solutions back to the caller.
The solver will search for a solution that satisfies all hard constraints and optimizes the soft constraints. Once the solver finishes, you can print the final schedule and its score.
public static void printSolution(FilmFestivalSchedule solution) {
System.out.println("Final score: " + solution.getScore());
sortedScreenings(solution)
.forEach(screening -> System.out.printf("%s | %-18s | %-26s | duration=%3d | audience=%3d | segment=%s%n",
screening.scheduleLabel(), screening.getScreen() == null ? "UNASSIGNED" : screening.getScreen().getName(),
screening.getTitle(), screening.getDurationMinutes(), screening.getExpectedAudience(),
screening.getAudienceSegment()));
}
FilmFestivalScheduleSupport.java
For this example we will get the following output:
Final score: 0hard/-151soft
Thu 18:00-20:55 | Grand Theater | After the Storm | duration=175 | audience=160 | segment=arthouse
Thu 20:30-22:00 | Rooftop Cinema | City of Sparks | duration= 90 | audience=100 | segment=documentary
Thu 20:30-22:35 | Warehouse Screen | Midnight Syntax | duration=125 | audience=120 | segment=cult
Fri 16:00-17:55 | Warehouse Screen | Neon Alley | duration=115 | audience=145 | segment=action
Fri 18:00-20:45 | Grand Theater | Ember Lake | duration=165 | audience=185 | segment=drama
Fri 18:00-20:20 | Rooftop Cinema | Silent Orbit | duration=140 | audience= 85 | segment=arthouse
Fri 20:30-22:05 | Warehouse Screen | Family Pixels | duration= 95 | audience=105 | segment=family
Sat 17:30-20:10 | Grand Theater | Opening Night: Glass Harbor | duration=160 | audience=210 | segment=arthouse
Sat 17:30-19:15 | Rooftop Cinema | Paper Monument | duration=105 | audience= 95 | segment=documentary
Sat 20:15-22:30 | Warehouse Screen | Rust and Stardust | duration=135 | audience=170 | segment=drama
UNASSIGNED | UNASSIGNED | Quantum Hearts | duration=170 | audience=150 | segment=sci-fi
The score will indicate how well the solution satisfies the constraints. The score is the sum of all the penalties and rewards from the constraints.
A penalize(HardSoftScore.ONE_HARD) subtracts one hard point for every match. A penalize(HardSoftScore.ONE_SOFT, _ -> 20) subtracts 20 soft points for every match. A reward(...) does the opposite and adds points.
Timefold always maximizes the score. It compares candidate solutions against each other and keeps moving toward better scores until the termination condition is reached. With HardSoftScore, the hard score is more important than the soft score. A solution with 0hard/-1000soft is better than -1hard/0soft, because any hard constraint violation is considered worse than any amount of soft preference trade-off. Among solutions with the same hard score, the solver then compares the soft score, where values closer to zero are better if you only use penalties. For example, 0hard/-10soft is better than 0hard/-151soft, and 0hard/5soft would be better than 0hard/0soft if your model used rewards.
In many planning models, the ideal score is 0hard/0soft: no hard rule violations and no soft penalties. In models that use rewards, the best score can be positive.
In this example we have a final score of 0hard/-151soft, which means that all hard constraints are satisfied, but there are some soft constraint penalties. The solver found a schedule that does not violate any hard rules, but it does have some undesirable features according to the soft constraints. For instance, the screening "Quantum Hearts" is unassigned, which contributes to the soft penalty.
Testing the constraints ¶
The constraints are the heart of the model, so it is important to test them thoroughly. Timefold provides a ConstraintVerifier that allows you to test your constraints in isolation without running the full solver. You can create unit tests that verify that specific scenarios trigger the expected penalties or rewards for each constraint. For example, you can test the screenConflict constraint by creating two screenings that are assigned to the same screen and have overlapping timeslots, and then verify that the constraint penalizes that scenario:
private final ConstraintVerifier<FilmFestivalConstraintProvider, FilmFestivalSchedule> constraintVerifier = ConstraintVerifier
.build(new FilmFestivalConstraintProvider(), FilmFestivalSchedule.class, Screening.class);
@Test
void screenConflictIsHard() {
Timeslot firstStart = new Timeslot("fri-18", LocalDateTime.of(2026, 10, 9, 18, 0),
LocalDateTime.of(2026, 10, 9, 18, 30));
Timeslot secondStart = new Timeslot("fri-20", LocalDateTime.of(2026, 10, 9, 20, 0),
LocalDateTime.of(2026, 10, 9, 20, 30));
Screen screen = new Screen("grand", "Grand Theater", 220, true, true, Set.of("fri-18", "fri-20"));
Screening left = new Screening("a", "Opening", "Eva Stone", Set.of("Eva Stone"), "arthouse", 200, 170, true, true,
true);
left.setTimeslot(firstStart);
left.setScreen(screen);
Screening right = new Screening("b", "Quantum Hearts", "Mina Alvarez", Set.of("Leila Haddad"), "sci-fi", 150, 110,
true, true, true);
right.setTimeslot(secondStart);
right.setScreen(screen);
this.constraintVerifier.verifyThat(FilmFestivalConstraintProvider::screenConflict).given(left, right)
.penalizesBy(1);
}
FilmFestivalConstraintProviderTest.java
Wrapping up ¶
Timefold Solver is a powerful and flexible tool for solving scheduling problems. By modeling your domain in Java, defining your constraints with the Constraint Streams API, and running the solver, you can find good solutions to complex scheduling challenges. The open source edition of Timefold Solver provides all the core features you need to get started, and you can always explore the paid versions for additional capabilities and support.
To learn more about Timefold Solver, check out the Getting Started guide. There you find more examples and also guides how to integrate Timefold Solver into Quarkus and Spring Boot applications.