eBook – Guide Spring Cloud – NPI EA (cat=Spring Cloud)
announcement - icon

Let's get started with a Microservice Architecture with Spring Cloud:

>> Join Pro and download the eBook

eBook – Mockito – NPI EA (tag = Mockito)
announcement - icon

Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.

Get started with mocking and improve your application tests using our Mockito guide:

Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Reactive – NPI EA (cat=Reactive)
announcement - icon

Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:

>> Join Pro and download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Jackson – NPI EA (cat=Jackson)
announcement - icon

Do JSON right with Jackson

Download the E-book

eBook – HTTP Client – NPI EA (cat=Http Client-Side)
announcement - icon

Get the most out of the Apache HTTP Client

Download the E-book

eBook – Maven – NPI EA (cat = Maven)
announcement - icon

Get Started with Apache Maven:

Download the E-book

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

eBook – RwS – NPI EA (cat=Spring MVC)
announcement - icon

Building a REST API with Spring?

Download the E-book

Course – LS – NPI EA (cat=Jackson)
announcement - icon

Get started with Spring and Spring Boot, through the Learn Spring course:

>> LEARN SPRING
Course – RWSB – NPI EA (cat=REST)
announcement - icon

Explore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:

>> The New “REST With Spring Boot”

Course – LSS – NPI EA (cat=Spring Security)
announcement - icon

Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.

I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.

You can explore the course here:

>> Learn Spring Security

Course – LSD – NPI EA (tag=Spring Data JPA)
announcement - icon

Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.

Get started with Spring Data JPA through the guided reference course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (cat=Spring Boot)
announcement - icon

Refactor Java code safely — and automatically — with OpenRewrite.

Refactoring big codebases by hand is slow, risky, and easy to put off. That’s where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.

Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions — one for newcomers and one for experienced users. You’ll see how recipes work, how to apply them across projects, and how to modernize code with confidence.

Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.

Course – LJB – NPI EA (cat = Core Java)
announcement - icon

Code your way through and build up a solid, practical foundation of Java:

>> Learn Java Basics

Partner – LambdaTest – NPI EA (cat= Testing)
announcement - icon

Distributed systems often come with complex challenges such as service-to-service communication, state management, asynchronous messaging, security, and more.

Dapr (Distributed Application Runtime) provides a set of APIs and building blocks to address these challenges, abstracting away infrastructure so we can focus on business logic.

In this tutorial, we'll focus on Dapr's pub/sub API for message brokering. Using its Spring Boot integration, we'll simplify the creation of a loosely coupled, portable, and easily testable pub/sub messaging system:

>> Flexible Pub/Sub Messaging With Spring Boot and Dapr

1. Overview

1.1. What Is Timefold Solver?

Timefold Solver is a pure Java planning solver AI. Timefold optimizes planning problems, such as the vehicle routing problem (VRP), maintenance scheduling, job shop scheduling, and school timetabling. It generates logistics plans that heavily reduce costs, improve service quality, and decrease the environmental footprint – often by as much as 25% – for complex, real-world scheduling operations.

Timefold is the continuation of OptaPlanner. It’s a form of mathematical optimization (in the broader Operations Research and Artificial Intelligence spaces) that supports constraints written as code.

1.2. What We Will Build

In this tutorial, let’s use Timefold Solver to optimize a simplified employee shift scheduling problem.

We’ll assign shifts to employees automatically, such that:

  • No employee has two shifts on the same day
  • Every shift is assigned to an employee who has the appropriate skill

Specifically, we will assign these five shifts:

  2030-04-01 06:00 - 14:00 (waiter)
  2030-04-01 09:00 - 17:00 (bartender)
  2030-04-01 14:00 - 22:00 (bartender)
  2030-04-02 06:00 - 14:00 (waiter)
  2030-04-02 14:00 - 22:00 (bartender)

To these three employees:

  Ann (bartender)
  Beth (waiter, bartender)
  Carl (waiter)

This is harder than it looks. Give it a try on paper.

2. Dependencies

The Timefold Solver artifacts on Maven Central are released under the Apache License. Let’s use them:

2.1. Plain Java

We add a dependency on timefold-solver-core and a test dependency on timefold-solver-test in Maven or Gradle:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>ai.timefold.solver</groupId>
            <artifactId>timefold-solver-bom</artifactId>
            <version>...</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
<dependencies>
    <dependency>
        <groupId>ai.timefold.solver</groupId>
        <artifactId>timefold-solver-core</artifactId>
    </dependency>
    <dependency>
        <groupId>ai.timefold.solver</groupId>
        <artifactId>timefold-solver-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

2.2. Spring Boot

In Spring Boot, we use the timefold-solver-spring-boot-starter dependency instead. It handles most of the solver configuration automatically, as we’ll see later, and allows configuring solver time and other properties in application.properties.

  1. Go to start.spring.io
  2. Click Add dependencies to add the Timefold Solver dependency
  3. Generate a project and open it in your favorite IDE

2.3. Quarkus

In Quarkus, similarly, we use the timefold-solver-quarkus dependency in code.quarkus.io for automatic solver configuration and application.properties support.

3. Domain Classes

The domain classes represent both the input data and output data. We create Employee and Shift classes, as well as a ShiftSchedule that contains the list of employees and shifts for a particular dataset.

3.1. Employee

An employee is a person we can assign to shifts. Each employee has a name and one or more skills.

The Employee class doesn’t need any Timefold annotation because it does not change during solving:

public class Employee {

    private String name;
    private Set<String> skills;

    public Employee(String name, Set<String> skills) {
        this.name = name;
        this.skills = skills;
    }

    @Override
    public String toString() {
        return name;
    }

    // Getters and setters
}

3.2. Shift

A shift is a job assignment for exactly one employee on a specific date from a start time to an end time. There can be two shifts at the same time. Each shift has one required skill.

Shift objects change during solving: Each shift is assigned to an employee. Timefold needs to know that. Only the employee field changes during solving. Therefore, we annotate the class with @PlanningEntity and the employee field with @PlanningVariable so Timefold knows it should fill in the employee for each shift:

@PlanningEntity
public class Shift {

    private LocalDateTime start;
    private LocalDateTime end;
    private String requiredSkill;

    @PlanningVariable
    private Employee employee;

    // A no-arg constructor is required for @PlanningEntity annotated classes
    public Shift() {
    }

    public Shift(LocalDateTime start, LocalDateTime end, String requiredSkill) {
        this(start, end, requiredSkill, null);
    }

    public Shift(LocalDateTime start, LocalDateTime end, String requiredSkill, Employee employee) {
        this.start = start;
        this.end = end;
        this.requiredSkill = requiredSkill;
        this.employee = employee;
    }

    @Override
    public String toString() {
        return start + " - " + end;
    }

    // Getters and setters
}

3.3. ShiftSchedule

A schedule represents a single dataset of employees and shifts. It is both the input and output for Timefold:

  • We annotate the ShiftSchedule class with @PlanningSolution so Timefold knows it represents the input and output.
  • We annotate the employees field with @ValueRangeProvider to tell Timefold it contains the list of employees from which it can pick instances to assign to Shift.employee.
  • We annotate the shifts field with @PlanningEntityCollectionProperty so Timefold finds all Shift instances to assign to an employee.
  • We include a score field with a @PlanningScore annotation. Timefold will fill this in for us. Let’s use a HardSoftScore so we can differentiate between hard and soft constraints later.

Now, let’s have a look at our class:

@PlanningSolution
public class ShiftSchedule {

    @ValueRangeProvider
    private List<Employee> employees;
    @PlanningEntityCollectionProperty
    private List<Shift> shifts;

    @PlanningScore
    private HardSoftScore score;

    // A no-arg constructor is required for @PlanningSolution annotated classes
    public ShiftSchedule() {
    }

    public ShiftSchedule(List<Employee> employees, List<Shift> shifts) {
        this.employees = employees;
        this.shifts = shifts;
    }

    // Getters and setters
}

4. Constraints

Without constraints, Timefold would assign all shifts to the first employee. That’s not a feasible schedule.

To teach it how to distinguish good and bad schedules, let’s add two hard constraints:

  • The atMostOneShiftPerDay() constraint checks if two shifts on the same date are assigned to the same employee. If that’s the case, it penalizes the score by 1 hard point.
  • The requiredSkill() constraint checks if a shift is assigned to an employee for which the shift’s required skill is part of the employee’s skill set. If it’s not, it penalizes the score by 1 hard point.

A single hard constraint takes priority over all soft constraints. Typically, hard constraints are impossible to break, either physically or legally. Soft constraints, on the other hand, can be broken, but we want to minimize that. Those typically represent financial costs, service quality, or employee happiness. Hard and soft constraints are implemented with the same API.

4.1. ConstraintProvider

First, we create a ConstraintProvider for our constraint implementations:

public class ShiftScheduleConstraintProvider implements ConstraintProvider {

    @Override
    public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
        return new Constraint[] {
          atMostOneShiftPerDay(constraintFactory),
          requiredSkill(constraintFactory)
        };
    }

    // Constraint implementations

}

4.2. Unit Test the ConstraintProvider

If it isn’t tested, it doesn’t work — especially for constraints. Let’s create a test class to test each constraint of our ConstraintProvider.

The test-scoped timefold-solver-test dependency contains ConstraintVerifier, a helper to test each constraint in isolation. This improves maintenance — we can refactor a single constraint without breaking tests of other constraints:

public class ShiftScheduleConstraintProviderTest {

    private static final LocalDate MONDAY = LocalDate.of(2030, 4, 1);
    private static final LocalDate TUESDAY = LocalDate.of(2030, 4, 2);

    ConstraintVerifier<ShiftScheduleConstraintProvider, ShiftSchedule> constraintVerifier
      = ConstraintVerifier.build(new ShiftScheduleConstraintProvider(), ShiftSchedule.class, Shift.class);

    // Tests for each constraint

}

We’ve also prepared two dates to reuse in our tests below. Let’s add the actual constraints next.

4.3. Hard Constraint: at Most One Shift per Day

Following TDD (Test Driven Design), let’s write the tests for our new constraint in our test class first:

@Test
void whenTwoShiftsOnOneDay_thenPenalize() {
    Employee ann = new Employee("Ann", null);
    constraintVerifier.verifyThat(ShiftScheduleConstraintProvider::atMostOneShiftPerDay)
      .given(
        new Shift(MONDAY.atTime(6, 0), MONDAY.atTime(14, 0), null, ann),
        new Shift(MONDAY.atTime(14, 0), MONDAY.atTime(22, 0), null, ann))
      // Penalizes by 2 because both {shiftA, shiftB} and {shiftB, shiftA} match.
      // To avoid that, use forEachUniquePair() in the constraint instead of forEach().join() in the implementation.
      .penalizesBy(2);
}

@Test
void whenTwoShiftsOnDifferentDays_thenDoNotPenalize() {
    Employee ann = new Employee("Ann", null);
    constraintVerifier.verifyThat(ShiftScheduleConstraintProvider::atMostOneShiftPerDay)
      .given(
        new Shift(MONDAY.atTime(6, 0), MONDAY.atTime(14, 0), null, ann),
        new Shift(TUESDAY.atTime(14, 0), TUESDAY.atTime(22, 0), null, ann))
      .penalizesBy(0);
}

Then, we implement it in our ConstraintProvider:

public Constraint atMostOneShiftPerDay(ConstraintFactory constraintFactory) {
    return constraintFactory.forEach(Shift.class)
      .join(Shift.class,
        equal(shift -> shift.getStart().toLocalDate()),
        equal(Shift::getEmployee))
      .filter((shift1, shift2) -> shift1 != shift2)
      .penalize(HardSoftScore.ONE_HARD)
      .asConstraint("At most one shift per day");
}

To implement constraints, we use the ConstraintStreams API: a Stream/SQL-like API that provides incremental score calculation (deltas) and indexed hashtable lookups under the hood. This approach scales to datasets with hundreds of thousands of shifts in a single schedule.

Let’s run the tests and verify they are green.

4.4. Hard Constraint: Required Skill

Let’s write the tests in our test class:

@Test
void whenEmployeeLacksRequiredSkill_thenPenalize() {
    Employee ann = new Employee("Ann", Set.of("Waiter"));
    constraintVerifier.verifyThat(ShiftScheduleConstraintProvider::requiredSkill)
      .given(
        new Shift(MONDAY.atTime(6, 0), MONDAY.atTime(14, 0), "Cook", ann))
      .penalizesBy(1);
}

@Test
void whenEmployeeHasRequiredSkill_thenDoNotPenalize() {
    Employee ann = new Employee("Ann", Set.of("Waiter"));
    constraintVerifier.verifyThat(ShiftScheduleConstraintProvider::requiredSkill)
      .given(
        new Shift(MONDAY.atTime(6, 0), MONDAY.atTime(14, 0), "Waiter", ann))
      .penalizesBy(0);
}

Then, let’s implement the new constraint in our ConstraintProvider:

public Constraint requiredSkill(ConstraintFactory constraintFactory) {
    return constraintFactory.forEach(Shift.class)
      .filter(shift -> !shift.getEmployee().getSkills()
        .contains(shift.getRequiredSkill()))
      .penalize(HardSoftScore.ONE_HARD)
      .asConstraint("Required skill");
}

Let’s run the tests again. They are still green.

To make this a soft constraint, we would change penalize(HardSoftScore.ONE_HARD) into penalize(HardSoftScore.ONE_SOFT). To turn that into a dynamic decision by the input dataset, we could use penalizeConfigurable() and @ConstraintWeight instead.

5. Application

We’re ready to put our application together.

5.1. Solve It

To solve a schedule, we create a SolverFactory from our @PlanningSolution, @PlanningEntity, and ConstraintProvider classes. A SolverFactory is a long-lived object. Typically, there’s only one instance per application.

We also need to configure how long we want a solver to run. For large datasets, with thousands of shifts and far more constraints, it’s impossible to find the optimal solution in a reasonable timeframe (due to the exponential nature of NP-hard problems). Instead, we want to find the best possible solution in the amount of time available. Let’s limit that to two seconds for now:

SolverFactory<ShiftSchedule> solverFactory = SolverFactory.create(new SolverConfig()
  .withSolutionClass(ShiftSchedule.class)
  .withEntityClasses(Shift.class)
  .withConstraintProviderClass(ShiftScheduleConstraintProvider.class)
  // The solver runs only for 2 seconds on this tiny dataset.
  // It's recommended to run for at least 5 minutes ("5m") on large datasets.
  .withTerminationSpentLimit(Duration.ofSeconds(2)));

We use the SolverFactory to create a Solver instance, one per dataset. Then, we call Solver.solve() to solve a dataset:

Solver<ShiftSchedule> solver = solverFactory.buildSolver();
ShiftSchedule problem = loadProblem();
ShiftSchedule solution = solver.solve(problem);
printSolution(solution);

In Spring Boot, the SolverFactory is built automatically and injected into an @Autowired field:

@Autowired
SolverFactory<ShiftSchedule> solverFactory;

And we configure the solver time in application.properties:

timefold.solver.termination.spent-limit=5s

In Quarkus, similarly, the SolverFactory is also built automatically and injected in an @Inject field. The solver time is also configured in application.properties.

To solve asynchronously, to avoid hogging the current thread when calling Solver.solve(), we would inject and use a SolverManager instead.

5.2. Test Data

Let’s generate a tiny dataset of five shifts and three employees as the input problem:

private ShiftSchedule loadProblem() {
    LocalDate monday = LocalDate.of(2030, 4, 1);
    LocalDate tuesday = LocalDate.of(2030, 4, 2);
    return new ShiftSchedule(List.of(
      new Employee("Ann", Set.of("Bartender")),
      new Employee("Beth", Set.of("Waiter", "Bartender")),
      new Employee("Carl", Set.of("Waiter"))
    ), List.of(
      new Shift(monday.atTime(6, 0), monday.atTime(14, 0), "Waiter"),
      new Shift(monday.atTime(9, 0), monday.atTime(17, 0), "Bartender"),
      new Shift(monday.atTime(14, 0), monday.atTime(22, 0), "Bartender"),
      new Shift(tuesday.atTime(6, 0), tuesday.atTime(14, 0), "Waiter"),
      new Shift(tuesday.atTime(14, 0), tuesday.atTime(22, 0), "Bartender")
    ));
}

5.3. Result

After we run the test data through our solver, we’ll print the output solution to System.out:

private void printSolution(ShiftSchedule solution) {
    logger.info("Shift assignments");
    for (Shift shift : solution.getShifts()) {
        logger.info("  " + shift.getStart().toLocalDate()
          + " " + shift.getStart().toLocalTime()
          + " - " + shift.getEnd().toLocalTime()
          + ": " + shift.getEmployee().getName());
    }
}

Here’s the result for our dataset:

Shift assignments
  2030-04-01 06:00 - 14:00: Carl
  2030-04-01 09:00 - 17:00: Ann
  2030-04-01 14:00 - 22:00: Beth
  2030-04-02 06:00 - 14:00: Beth
  2030-04-02 14:00 - 22:00: Ann

Ann wasn’t assigned to the first shift because she didn’t have the waiter skill. But why wasn’t Beth assigned to the first shift? She has the waiter skill.

If Beth had been assigned to the first shift, it would then be impossible to assign both the second and third shifts. Those both need a bartender, so Carl can’t do them. Only when Carl is assigned to the first shift is a feasible solution possible. In large, real-world datasets, these kinds of intricacies become a lot more complex. Let the Solver worry about them.

6. Conclusion

The Timefold Solver framework provides developers with a powerful tool to solve constraint satisfaction problems such as scheduling and resource allocation. It supports writing custom constraints in code (instead of mathematical equations), which makes it maintenance-friendly. Under the hood, it supports various Artificial Intelligence optimization algorithms that can be power-tweaked, but a typical user doesn’t need to do so.

For more information, see the Timefold Solver documentation.

The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.
Baeldung Pro – NPI EA (cat = Baeldung)
announcement - icon

Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:

>> Explore a clean Baeldung

Once the early-adopter seats are all used, the price will go up and stay at $33/year.

eBook – HTTP Client – NPI EA (cat=HTTP Client-Side)
announcement - icon

The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:

>> Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

Course – LS – NPI EA (cat=REST)

announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (tag=Refactoring)
announcement - icon

Modern Java teams move fast — but codebases don’t always keep up. Frameworks change, dependencies drift, and tech debt builds until it starts to drag on delivery. OpenRewrite was built to fix that: an open-source refactoring engine that automates repetitive code changes while keeping developer intent intact.

The monthly training series, led by the creators and maintainers of OpenRewrite at Moderne, walks through real-world migrations and modernization patterns. Whether you’re new to recipes or ready to write your own, you’ll learn practical ways to refactor safely and at scale.

If you’ve ever wished refactoring felt as natural — and as fast — as writing code, this is a good place to start.

eBook Jackson – NPI EA – 3 (cat = Jackson)