Docs
  • Solver
  • Models
    • Field Service Routing
    • Employee Shift Scheduling
    • Pick-up and Delivery Routing
    • Task Scheduling
  • Platform
Try models
  • Timefold Solver SNAPSHOT
  • Running the Solver
  • As a service
  • Model configuration overrides
  • Edit this Page

Timefold Solver SNAPSHOT

    • Introduction
    • Getting started
      • Overview
      • Build as a service
      • Embed as a library
        • Hello World guide
        • Quarkus guide
        • Spring Boot guide
    • Domain modeling
      • Guide
      • Building blocks
      • Common patterns
    • Constraints and score
      • Overview
      • Score calculation
      • Understanding the score
      • Load balancing and fairness
      • Performance tips and tricks
    • Running the Solver
      • Overview
      • As a service
        • REST API
        • Model configuration overrides
        • Model enrichment
        • Demo data
        • Exposing metrics
        • Service consumer guide
      • As a library
        • Configuring Timefold Solver
        • Constraint weights
        • Quarkus integration
        • Spring Boot integration
        • JPA/JAXB/JSON integration
    • Diagnosing the Solver
      • Benchmarking
      • Solver diagnostics
    • Deploying to the Timefold Platform
      • Overview
      • Guide
      • Platform model metadata
      • Using metrics
    • Optimization algorithms
      • Overview
      • Construction heuristics
      • Local search
      • Exhaustive search
      • Custom moves
        • Neighborhoods API
        • Move Selector reference
    • Responding to change
      • Continuous planning
      • Real-time planning
      • Non-disruptive replanning
      • Assignment Recommendation API
    • Example use cases
      • Vehicle routing (guide)
      • More examples on GitHub
    • FAQ
    • New and noteworthy
    • Upgrading Timefold Solver
      • Upgrading Timefold Solver: Overview
      • Upgrade from Timefold Solver 1.x to 2.x
      • Upgrading from OptaPlanner
      • Backwards compatibility
      • Migration guides
        • Variable Listeners to Custom Shadow Variables
        • Chained planning variable to planning list variable
    • Commercial editions
      • Overview
      • Installation
      • Performance improvements
      • Score analysis
      • Recommendation API
      • Nearby selection
      • Multithreaded solving
      • Partitioned search
      • Constraint profiling
      • Multistage moves
      • Throttling best solution events
      • License management

Model configuration overrides

Model configuration overrides allow callers to tune the solver’s objective function per request — adjusting how much weight is given to individual constraints, and passing constraint-specific parameters — without modifying the solver model itself.

Three components work together to deliver this:

Component Role

ModelConfigOverrides

Marker interface. Your implementing class carries the tunable fields callers can set per request.

@ConstraintReference

Links a constraint weight multiplier field to a specific constraint.

ModelConfig<T>

Runtime container. Carries the caller’s overrides instance into ModelConvertor.toSolverModel().

Deciding the correct weight and level for each constraint is not easy. It often involves negotiating with different stakeholders and their priorities. Furthermore, quantifying the impact of soft constraints is often a new experience for business managers, so they’ll need a number of iterations to get it right.

Don’t get stuck between a rock and a hard place. Provide a UI to adjust the constraint weights and visualize the resulting solution, so the business managers can tweak the constraint weights themselves:

parameterizeTheScoreWeights

1. Defining constraint weights

Let’s define three constraints:

  • Constraint with a name of Vehicle capacity and a weight of 1hard.

  • Constraint with a name of Service finished after max end time, also with a weight of 1hard.

  • Constraint with a name of Minimize travel time and a weight of 1soft.

Using the Constraint Streams API, this is done as follows:

  • Java

public class VehicleRoutingConstraintProvider implements ConstraintProvider {

    public static final String VEHICLE_CAPACITY = "Vehicle capacity";
    public static final String SERVICE_FINISHED_AFTER_MAX_END_TIME = "Service finished after max end time";
    public static final String MINIMIZE_TRAVEL_TIME = "Minimize travel time";

    ...

    @Override
    public Constraint[] defineConstraints(ConstraintFactory factory) {
        return new Constraint[] {
                vehicleCapacity(factory),
                serviceFinishedAfterMaxEndTime(factory),
                minimizeTravelTime(factory)
        };
    }

    Constraint vehicleCapacity(ConstraintFactory factory) {
        return factory.forEach(Vehicle.class)
                ...
                .penalize(HardSoftScore.ONE_HARD, ...)
                .asConstraint(VEHICLE_CAPACITY);
    }

    Constraint serviceFinishedAfterMaxEndTime(ConstraintFactory factory) {
        return factory.forEach(Visit.class)
                ...
                .penalize(HardSoftScore.ONE_HARD, ...)
                .asConstraint(SERVICE_FINISHED_AFTER_MAX_END_TIME);
    }

    Constraint minimizeTravelTime(ConstraintFactory factory) {
        return factory.forEach(Vehicle.class)
                ...
                .penalize(HardSoftScore.ONE_SOFT, ...)
                .asConstraint(MINIMIZE_TRAVEL_TIME);
    }
}
Using static string constants for constraint names is recommended. It prevents typos and ensures the constraint name is consistent across the ConstraintProvider and any code that references constraints by name, such as when applying overrides.

2. Enabling weight overrides

Without anything else, the constraint weights are fixed to the values specified in the ConstraintProvider. To be able to override these weights at runtime, add a ConstraintWeightOverrides field to the planning solution class:

  • Java

@PlanningSolution
public class VehicleRoutePlan {

    ...

    ConstraintWeightOverrides<HardSoftScore> constraintWeightOverrides;

    void setConstraintWeightOverrides(ConstraintWeightOverrides<HardSoftScore> constraintWeightOverrides) {
        this.constraintWeightOverrides = constraintWeightOverrides;
    }

    ConstraintWeightOverrides<HardSoftScore> getConstraintWeightOverrides() {
        return constraintWeightOverrides;
    }

    ...
}

The field will be automatically exposed as a problem fact, there is no need to add a @ProblemFactProperty annotation.

3. ModelConfigOverrides

ModelConfigOverrides is a marker interface from ai.timefold.solver.service.definition.api. Implement it with a plain class that declares all fields a caller can override per request.

Fields linked to a specific constraint are annotated with @ConstraintReference. Fields that represent global configuration, not tied to any single constraint, require no annotation.

  • Java

  • Kotlin

public final class MyPlanConfigOverrides implements ModelConfigOverrides {

    // Constraint weight fields
    @ConstraintReference(MyConstraints.MINIMIZE_TRAVEL_TIME)
    private long minimizeTravelTimeWeight = 1L;

    @ConstraintReference(MyConstraints.PREFER_EARLY_COMPLETION)
    private long preferEarlyCompletionWeight = 0L;

    // Global config — not constraint-specific, no annotation required
    private double travelTimeAdjustment;

    // Standard getters, setters, and optional fluent "with" builders ...
}
data class MyPlanConfigOverrides(
    @ConstraintReference(MyConstraints.MINIMIZE_TRAVEL_TIME)
    val minimizeTravelTimeWeight: Long = 1L,

    @ConstraintReference(MyConstraints.PREFER_EARLY_COMPLETION)
    val preferEarlyCompletionWeight: Long = 0L,

    val travelTimeAdjustment: Double? = null
) : ModelConfigOverrides

Non-constraint global configuration that needs to vary per request can also live in this class.

Be careful not to make your model overly configurable as that impacts usability. Usually, it doesn’t make sense to allow weight overrides for hard constraints.

4. @ConstraintReference

@ConstraintReference wires a field in your ModelConfigOverrides implementation to a named constraint.

Attribute Type Default Purpose

value

String

(required)

The constraint name. Must match the identifier used in your ConstraintProvider.

4.1. Weight multipliers

Annotate a long field to make it a weight multiplier for that constraint. The value scales the constraint’s score contribution relative to other soft constraints.

A value of 0 effectively disables the constraint for that request. Higher values increase the constraint’s priority relative to others. Do keep the value significantly below Long.MAX_VALUE, since multiple constraints keep adding to the score impact across all their matches.

// Active by default
@ConstraintReference(MyConstraints.MINIMIZE_TRAVEL_TIME)
private long minimizeTravelTimeWeight = 1L;

// Disabled by default; callers can enable it by setting the weight above 0
@ConstraintReference(MyConstraints.PREFER_GROUPING_CO_LOCATED_VISITS)
private long preferGroupingCoLocatedVisitsWeight = 0L;

5. Applying overrides in ModelConvertor

The ModelConvertor implementation translates the caller’s overrides into the data structures the solver engine expects. It receives a ModelConfig<T> in toSolverModel() and must handle the case where the caller provided no overrides at all — modelConfig itself, or modelConfig.overrides(), may be null.

This requires a custom ModelConvertor. If your SolverModel also serves as ModelInput and ModelOutput, for example because you extend AbstractSimpleModel, the framework uses a trivial ModelConvertor by default, which ignores modelConfig entirely. With only the trivial convertor in place, constraint weight overrides submitted in a request are silently dropped: the request succeeds, but the override never reaches the solver. Provide your own ModelConvertor implementation, as shown below, to actually apply them.

5.1. Reading constraint weights

Build a Map<String, Score> from constraint names to score values, then pass it to ConstraintWeightOverrides.of(…​) when constructing the solver model.

When no overrides are provided, fall back to a default-constructed instance of your overrides class so that all defaults are centralised in one place.

  • Java

  • Kotlin

private static Map<String, HardMediumSoftScore>
        getConstraintWeightOverrides(ModelConfig<MyPlanConfigOverrides> modelConfig) {

    MyPlanConfigOverrides overrides =
            modelConfig != null && modelConfig.overrides() != null
                    ? modelConfig.overrides()
                    : new MyPlanConfigOverrides(); // defaults come from field initialisers

    return Map.ofEntries(
            Map.entry(MyConstraints.MINIMIZE_TRAVEL_TIME,
                    HardMediumSoftScore.ofSoft(overrides.getMinimizeTravelTimeWeight())),
            Map.entry(MyConstraints.PREFER_EARLY_COMPLETION,
                    HardMediumSoftScore.ofSoft(overrides.getPreferEarlyCompletionWeight())),
            Map.entry(MyConstraints.MINIMIZE_COMPLETION_RISK,
                    HardMediumSoftScore.ofSoft(overrides.getMinimizeCompletionRiskWeight()))
    );
}
private fun getConstraintWeightOverrides(
        modelConfig: ModelConfig<MyPlanConfigOverrides>?): Map<String, HardMediumSoftScore> {

    val overrides = modelConfig?.overrides() ?: MyPlanConfigOverrides()

    return mapOf(
            MyConstraints.MINIMIZE_TRAVEL_TIME to
                    HardMediumSoftScore.ofSoft(overrides.minimizeTravelTimeWeight),
            MyConstraints.PREFER_EARLY_COMPLETION to
                    HardMediumSoftScore.ofSoft(overrides.preferEarlyCompletionWeight),
            MyConstraints.MINIMIZE_COMPLETION_RISK to
                    HardMediumSoftScore.ofSoft(overrides.minimizeCompletionRiskWeight)
    )
}

5.2. Reading constraint parameters

Extract parameter fields individually into a value object, or pass them directly to the solver model. Guard against null the same way as for weights.

private static ConstraintParameters
        getConstraintParameters(ModelConfig<MyPlanConfigOverrides> modelConfig) {

    if (modelConfig == null || modelConfig.overrides() == null) {
        return null;
    }
    MyPlanConfigOverrides overrides = modelConfig.overrides();
    return new ConstraintParameters(
            overrides.getBufferBeforeShiftEnd(),
            overrides.getMinimumPriorityLevel()
    );
}

Apply both in toSolverModel():

@Override
public MySolverModel toSolverModel(
        MyPlanInput input,
        ModelConfig<MyPlanConfigOverrides> modelConfig,
        Optional<MyPlanOutput> previousSolution) {

    Map<String, HardMediumSoftScore> weightOverrides = getConstraintWeightOverrides(modelConfig);
    ConstraintParameters constraintParams = getConstraintParameters(modelConfig);

    return new MySolverModel(...)
            .withConstraintWeightOverrides(ConstraintWeightOverrides.of(weightOverrides))
            .withConstraintParameters(constraintParams);
}

5.3. ConstraintParameters in the solver model

The ConstraintParameters object is stored on the planning solution class and annotated with @ProblemFactProperty, which makes it visible to the constraint engine.

@ProblemFactProperty
private ConstraintParameters constraintParameters;

Because it is a problem fact and not a planning entity, the constraint engine does not iterate over it automatically. Constraints that need the parameter values must explicitly join against ConstraintParameters.class in their constraint stream.

protected Constraint minimizeCompletionRisk(ConstraintFactory factory) {
    return factory.forEach(MyVisit.class)
            .join(MyShift.class,
                    Joiners.equal(MyVisit::getShift, Function.identity()))
            .join(ConstraintParameters.class)  (1)
            .filter((visit, shift, params) ->
                    visit.getDepartureTime().isAfter(
                            shift.getEndTime().minus(params.bufferBeforeShiftEnd())))
            .penalize(HardMediumSoftScore.ONE_SOFT, ...)
            .asConstraint(MyConstraints.MINIMIZE_COMPLETION_RISK);
}
1 The join has no Joiners condition because there is exactly one ConstraintParameters instance. Every tuple from the preceding stream is paired with it unconditionally.

Constraints that do not use any parameter values do not need this join. Only add it where the constraint logic actually reads from ConstraintParameters.

6. Conventions

  • Weight fields are long with a positive default. Use 0L to disable a constraint by default; use 1L or higher to enable it. Weight values must be non-negative.

  • Parameter fields carry a sensible domain default. Use null only when the absence of a value is meaningful to the constraint. Otherwise initialise to a safe zero-value. e.g. Duration.ZERO, 0.0, the lowest tier string, and so on.

  • Constraint names are constants. Define every constraint name as a public static final String in your ConstraintProvider. Never use inline string literals in @ConstraintReference.

  • Global configuration is separate from constraints. Fields not linked to a specific constraint require no @ConstraintReference annotation. Extract and apply them independently in the convertor.

  • Fluent builders are optional but encouraged. Providing withX() methods on your overrides class makes test setup and programmatic request construction significantly more readable.

  • © 2026 Timefold BV
  • Timefold.ai
  • Documentation
  • Changelog
  • Send feedback
  • Privacy
  • Legal
    • Light mode
    • Dark mode
    • System default