Model enrichment
1. What is model enrichment?
Model enrichment is the process of adding extra information to the SolverModel before solving starts.
The service receives the planning problem as submitted through the REST API,
but sometimes that raw model is incomplete: it may be missing derived fields, external lookups, or pre-computed values
that are too expensive or impractical to calculate during solving.
Enrichers run once, just before the solver begins, and produce an augmented model that constraints can use directly.
Common use cases include:
-
Looking up data from an external service or database (e.g. travel times, holiday calendars, price lists).
-
Pinning entities that represent work already completed or committed.
-
Pre-computing expensive derived values (e.g. distance matrices, compatibility scores) so constraints stay fast.
2. Why enrich the model instead of computing inside constraints?
Constraint streams execute many times per second during solving. Any computation that can be done once before solving starts should be moved into model enrichment.
| Never do computations that involve remote calls, database queries, or complex algorithms while the solver is running (e.g. inside constraint streams or shadow variables). This is an antipattern and will make the solver unusably slow. |
3. Best practices
- Do
-
-
Make enrichers idempotent: calling
enrich()twice should produce the same result. -
Fail fast: if a required external resource is unavailable, throw an exception rather than silently returning an incomplete model.
-
Use the
SolverModelEnrichmentDirectorto control ordering when one enricher depends on another.
-
- Don’t
-
-
Do not perform heavy computation inside constraint streams. Move it to an enricher instead.
-
Do not keep mutable state in enrichers; they may be invoked concurrently from multiple threads.
-
Do not use enrichment for validation that should happen at input time; reject bad input at the API boundary, not here.
-
4. Implementing an enricher
To enrich the model, implement SolverModelEnricher and annotate it so it is picked up by your Dependency Injection (DI) container.
In this example, the Timetable planning solution contains Timeslot objects.
Each Timeslot has an isHoliday field that must be populated by consulting an external calendar service.
-
Java
-
Kotlin
public class Timeslot {
private LocalDateTime startTime;
private LocalDateTime endTime;
private boolean isHoliday; (1)
public void setHoliday(boolean isHoliday) {
this.isHoliday = isHoliday;
}
// other Getters/Setters/Constructors excluded
}
| 1 | This field is not provided by the caller — it is populated by the enricher. |
data class Timeslot(
val startTime: LocalDateTime? = null,
val endTime: LocalDateTime? = null
) {
var isHoliday: Boolean = false (1)
}
| 1 | This field is not provided by the caller — it is populated by the enricher. |
-
Java
-
Kotlin
@ApplicationScoped
public class TimeslotHolidayEnricher implements SolverModelEnricher<Timetable> {
@Override
public Timetable enrich(Timetable solverModel) {
for (Timeslot timeslot : solverModel.getTimeslots()) {
boolean isHoliday = overlapsWithKnownHoliday(timeslot.getStartTime(), timeslot.getEndTime());
timeslot.setHoliday(isHoliday);
}
return solverModel;
}
private boolean overlapsWithKnownHoliday(LocalDateTime start, LocalDateTime end) {
// Call an external service or database here.
return false;
}
}
@ApplicationScoped
class TimeslotHolidayEnricher : SolverModelEnricher<Timetable> {
override fun enrich(solverModel: Timetable): Timetable {
for (timeslot in solverModel.timeslots) {
timeslot.isHoliday = overlapsWithKnownHoliday(timeslot.startTime, timeslot.endTime)
}
return solverModel
}
private fun overlapsWithKnownHoliday(start: LocalDateTime?, end: LocalDateTime?): Boolean {
// Call an external service or database here.
return false
}
}
5. Transport types
Models whose solution implements LocationsAwareSolverModel are enriched by the built-in TravelTimeMatrixEnricher,
which fetches travel time and distance matrices from the map service and stores them on each Location.
Which routing profile those matrices describe is controlled by the transport type:
| Value | Meaning |
|---|---|
|
The default. Distances follow the road network as driven by a car. |
|
Distances follow the cycling network. |
|
Distances follow the walking network. |
The platform sets the transport type per deployment.
A model can declare its default with timefold.model.default-config.map.transport-type,
see model metadata.
When the deployment fixes a transport type, a dataset that uses another one is rejected before any map service call is made, because a deployment only has the map for the profile it is configured with. Otherwise, the transport types can be taken from the dataset; see Routing one dataset with several transport types.
5.1. Reading travel times and distances
Location exposes an overload of each lookup that takes the transport type.
Use it whenever your model distinguishes between transport types:
TravelTime travelTime = origin.getTravelTimeTo(destination, TransportType.BICYCLE);
TravelDistance distance = origin.getDistanceTo(destination, TransportType.BICYCLE);
// The traffic-aware overloads take the departure time as well.
TravelTime atNoon = origin.getTravelTimeTo(destination, departureTime, TransportType.BICYCLE);
The overloads without a transport type keep working and use the default transport type.
5.2. Routing one dataset with several transport types
When the deployment does not fix a transport type, the transport types come from the dataset.
The enricher asks the solution which ones the dataset actually uses, by overriding getTransportTypes():
@Override
public List<TransportType> getTransportTypes() {
return vehicles.stream()
.map(Vehicle::getTransportType)
.filter(Objects::nonNull)
.distinct()
.toList();
}
Each of those transport types is fetched in its own map service request, so a dataset using two transport types
results in two sets of matrices per Location.
A dataset that uses a transport type the tenant is not allowed to route with is rejected before any request is made.
When the solution returns an empty list, the default transport type is used.
6. Controlling enrichment order
When you have multiple enrichers, register a SolverModelEnrichmentDirector to control the order in which they run.
This matters when one enricher builds on the output of another.
-
Java
-
Kotlin
@ApplicationScoped
public class TimetableEnrichmentDirector implements SolverModelEnrichmentDirector<Timetable> {
private final TimeslotHolidayEnricher timeslotEnricher;
@Inject
public TimetableEnrichmentDirector(TimeslotHolidayEnricher timeslotEnricher) {
this.timeslotEnricher = timeslotEnricher;
}
@Override
public Timetable enrich(Timetable solverModel) {
var enrichedModel = timeslotEnricher.enrich(solverModel);
// Chain additional enrichers here, in dependency order.
return enrichedModel;
}
}
@ApplicationScoped
class TimetableEnrichmentDirector @Inject constructor(
private val timeslotEnricher: TimeslotHolidayEnricher
) : SolverModelEnrichmentDirector<Timetable> {
override fun enrich(solverModel: Timetable): Timetable {
val enrichedModel = timeslotEnricher.enrich(solverModel)
// Chain additional enrichers here, in dependency order.
return enrichedModel
}
}