Introduction
The Organization Decision Record (ODR) framework provides a structured approach to decision-making by clearly defining context, constraints, and requirements. However, as decision complexity increases, manually evaluating all possible options becomes impractical. Constraint Satisfaction Problems (CSP) offer a way to systematically optimize decision-making by leveraging constraint-solving algorithms.
In this article, we’ll explore how to convert an ODR decision into a CSP problem and implement it using Timefold, an open-source optimization engine.
Understanding the Decision-Making Problem
Let’s take the example from the original ODR article:
Context
- Team A lacks a Product Owner (PO).
- Team B has a PO.
- Hiring externally takes time.
Constraints
- Each team must have a PO.
- No PO can be assigned to more than one team.
- Hiring externally has a higher cost.
Considered Options
- Fire the whole team (unrealistic but considered).
- Hire an external PO.
- Open a backfill position.
- Hire a contractor.
- Transfer an internal PO.
- Transfer John Doe from Team A.
- Transfer Jane Doe from Team B.
Rather than manually scoring these options, we can formulate this as a Constraint Satisfaction Problem (CSP) and solve it with Timefold.
Defining the CSP Model
Step 1: Define Decision Variables
The decision variables represent the possible assignments of a PO to Team A:
poAssignment
can take values{External, John Doe, Jane Doe, None}
.
Step 2: Define Constraints
Using Timefold’s constraint engine, we encode:
- Each team must have a PO → A valid solution must assign a PO to Team A.
- No PO can be assigned twice → If Jane Doe is assigned to Team A, she cannot remain in Team B.
- Hiring cost minimization → Assigning an internal PO is preferred over hiring externally.
Step 3: Define an Optimization Function
The objective is to minimize hiring cost while ensuring every team has a PO.
Implementing the CSP Model with Timefold
@PlanningEntity
public class Team {
private String name;
@PlanningVariable(valueRangeProviderRefs = "poRange")
private ProductOwner assignedPO;
}
@PlanningSolution
public class ODRSolution {
@PlanningEntityCollectionProperty
private List<Team> teams;
@ValueRangeProvider(id = "poRange")
public List<ProductOwner> getAvailablePOs() {
return List.of(new ProductOwner("John Doe"), new ProductOwner("Jane Doe"), new ProductOwner("External", true));
}
}
public class ODRConstraintProvider implements ConstraintProvider {
@Override
public Constraint[] defineConstraints(ConstraintFactory factory) {
return new Constraint[] {
factory.forEach(Team.class)
.filter(team -> team.getAssignedPO() == null)
.penalize("Each team must have a PO", HardSoftScore.ONE_HARD),
factory.forEachUniquePair(Team.class, Joiners.equal(Team::getAssignedPO))
.penalize("No PO duplication", HardSoftScore.ONE_HARD),
factory.forEach(Team.class)
.filter(team -> team.getAssignedPO() != null && team.getAssignedPO().isExternal())
.penalize("Minimize external hiring cost", HardSoftScore.ONE_SOFT)
};
}
}
Running the Solver
SolverFactory<ODRSolution> solverFactory = SolverFactory.create(new SolverConfig()
.withSolutionClass(ODRSolution.class)
.withEntityClasses(Team.class)
.withConstraintProviderClass(ODRConstraintProvider.class)
.withTerminationSpentLimit(Duration.ofSeconds(10)));
Solver<ODRSolution> solver = solverFactory.buildSolver();
ODRSolution bestSolution = solver.solve(initialSolution);
Conclusion
By formulating ODR decisions as a CSP, we can leverage constraint solvers like Timefold to automate optimal decision-making.
This approach scales efficiently, making it especially useful for complex organizational decisions. If you’re dealing with multiple teams, roles, or constraints, CSP-based optimization could be a game-changer!