Using DDD Architecture with Benders Decomposition
Benders decomposition keeps one group of decisions in a master problem and places the remaining decisions and difficult constraints in a subproblem. DDD preserves existing domain ownership across that split. The same Contexts can participate in a monolithic model, the master, or the subproblem through selective registration, while typed fixed-variable and cut protocols connect both sides.
1. Mathematical structure
Consider this minimization problem.
After fixing
Its dual is:
The master introduces
2. Choosing the decomposition boundary
The boundary is a mathematical decision before it is a code-organization decision. Good master candidates normally include:
- discrete decisions that determine the global combinatorial structure;
- relatively tight constraints that provide a useful lower bound;
- coupling variables that the subproblem must fix;
- a
variable that underestimates recourse cost.
Good subproblem candidates normally include:
- continuous variables that can be optimized after the master decision is fixed;
- expensive but structurally stable feasibility or recourse constraints;
- a part that can generate valid cuts from a dual solution or infeasibility certificate;
- components separable by scenario, resource, or time period.
Do not move a package into the subproblem merely because it is named “security” or “capacity.” First establish the fixed-
3. Context responsibilities
| Component | Main responsibility | External protocol |
|---|---|---|
| Master Contexts | Register | Master candidate solution |
| Subproblem Contexts | Register | Subproblem status, value, and certificate |
| Fixed-variable mapper | Map domain variables to candidate values | fixedVariables |
| Cut factory | Convert a dual solution/ray into a named domain cut | Feasibility or optimality cut |
| Application/algorithm service | Iterate, manage bounds, stop, guard quality, and fall back | Domain solution and diagnostics |
Domain objects own variables and intermediate values; a Context decides which model receives them:
class StowageContext {
fun register(model: AbstractLinearMetaModel<Flt64>): Try = TODO()
fun registerForBendersMP(
model: AbstractLinearMetaModel<Flt64>
): Try = TODO()
fun registerForBendersSP(
model: AbstractLinearMetaModel<Flt64>,
fixedVariables: Map<AbstractVariableItem<*, *>, Flt64>
): Try = TODO()
}These entry points may share aggregates and pipelines, but each must document which variables, intermediate values, objectives, and constraints it registers. Correctness must not depend on one entry point having happened to run first.
4. Fixed-variable protocol
fixedVariables means “master domain variable → current candidate value,” not “master column number → subproblem column number”:
val fixedVariables =
mutableMapOf<AbstractVariableItem<*, *>, Flt64>()
for (item in items.indices) {
for (position in positions.indices) {
fixedVariables[stowage.x[item, position]] =
masterSolution[stowage.x[item, position]]
}
}The protocol should guarantee:
- both sides use the same domain-variable identity or an explicit stable key;
- every coupling variable needed by the subproblem has a value;
- discrete values use one rounding tolerance, while raw values remain available for diagnostics;
- fixing affects only the current subproblem iteration;
- missing, duplicate, or out-of-range values fail immediately rather than silently becoming zero.
Intermediate values can remain context interfaces, but registering every intermediate value in both meta-models does not automatically create a mapping. Master/subproblem communication must be explicit.
5. Selective registration
One domain context may participate in several solve paths:
| Path | Registration method | Purpose |
|---|---|---|
| Monolithic MILP | register | Full benchmark or fallback model |
| Benders master | registerForBendersMP | Master variables, tight rows, and master objective |
| Benders subproblem | registerForBendersSP | Recourse variables, fixed relations, and subproblem objective |
Implement selective registration in the Context or pipeline composition, not by copying formulas into the Application. A business mode may bind different implementations to the same intermediate-value semantics, but the published meaning must remain stable.
6. Iteration lifecycle
- initialize domain objects and participating Contexts;
- construct and register the master and subproblem separately;
- solve the master for
, , and a lower bound; - fix
in the subproblem through fixedVariables; - solve the subproblem and classify the result:
- optimal: extract a dual extreme point and create an optimality cut;
- infeasible: extract a Farkas certificate/extreme ray and create a feasibility cut;
- unbounded, timeout, or solver error: use a separate failure policy;
- recheck cut coefficients and sense in the domain layer, then add the cut to the master;
- update bounds, gap, cut metrics, and the convergence trace;
- return to step 3 until converged;
- analyze the final master solution and recover
from the subproblem when needed; - apply quality guards and, when policy permits, fall back to the complete MILP.
while (iteration < config.maxIterations) {
val master = solveMaster()
val fixed = mapFixedVariables(master)
val sub = solveSubproblem(fixed)
when (sub.status) {
Optimal -> addOptimalityCut(cutFactory.fromDual(sub))
Infeasible -> addFeasibilityCut(cutFactory.fromRay(sub))
else -> return handleSubproblemFailure(sub)
}
updateBounds(master, sub)
if (converged()) break
iteration += 1
}This pseudocode describes responsibilities only. Reliable dual values, Farkas certificates, and incremental row addition are necessary backend capabilities for a strict implementation.
7. Bounds, convergence, and duplicate cuts
For minimization, the master objective normally supplies a lower bound:
When the subproblem is feasible, the candidate supplies an upper bound:
Absolute and relative gaps may be defined as:
At termination, check all of the following:
and are valid and ordered correctly; - the gap satisfies configured tolerances;
- the latest cuts are not materially violated;
- no artificial slack or unacceptable fallback remains;
- numerical noise is not repeatedly adding the same cut.
A cut should have a normalized stable signature and record its source context, iteration, type, violation, and dual certificate.
8. Multiple subproblems
When the subproblem separates by scenario
- single-cut: one
and one aggregate cut; smaller master, often slower convergence; - multi-cut: a
and independent cuts per scenario; larger master, stronger information; - parallel solves: one independent subproblem per scenario, with deterministic aggregation in the Application.
In every design, scenario probabilities, objective weights, and missing-scenario failure policy belong in the domain protocol, not only in thread-orchestration code.
9. Quality guards and fallback
A production application should not equate “solver returned success” with sufficient quality. Monitor:
- Benders gap over its limit;
- time or iteration limits;
- no progress over several rounds;
- weak bound improvement per cut;
- abnormal objective trajectory or repeated cuts;
- missing certificates or numerical instability in the subproblem.
When policy permits, failure or insufficient quality can fall back to a complete MILP. The fallback model must reuse the same Contexts through their register path and report the fallback reason; it must not assemble a second, unverified set of formulas.
10. Verification checklist
Decomposition equivalence
- build both the complete MILP and Benders models on a small instance, then compare feasibility and optimal objective;
- for a fixed
, compare the subproblem value with a manual calculation; - verify that the union of MP/SP selective registrations covers the complete model and that their intersection contains only intentionally shared semantics.
Cut tests
- an optimality cut gives the correct lower bound at its generating point
; - a feasibility cut rejects the current infeasible
without rejecting known feasible solutions; - dual signs, constant terms, and
coefficients follow backend conventions; - naming, deduplication, and tolerance boundaries are stable.
Lifecycle tests
fixedVariablesis complete and fresh, with a consistent integrality tolerance;- each
is nondecreasing and incumbent is nonincreasing for minimization, within tolerance; - cover optimal, infeasible, unbounded, timeout, and backend-error outcomes separately;
- quality guards trigger the specified failure or MILP fallback path.
11. Common mistakes
- choosing the split from package names rather than the coupling matrix;
- treating an infeasible subproblem as an algorithm failure instead of generating a feasibility cut;
- mapping variables by positions in the master and subproblem models;
- duplicating MP/SP constraints in the Application;
- computing a gap from incomparable objective values;
- claiming strict Benders cuts when the backend provides no valid certificate;
- using different domain rules in the fallback model.