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 happens inside a constraint is repeated on every score calculation. If that computation involves a remote call, a database query, or a complex algorithm, it will dominate solving time and 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 modify planning entities' planning variables inside an enricher; only non-planning fields should be set.
-
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 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. 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
}
}