When Local Improvements Lie
A supply chain decision can improve one SKU, scope, or echelon while making the total system worse. Learn how to optimize coupled decisions without fooling yourself with local metrics.
The Most Dangerous Improvement Is the One That Looks Obvious
A planner changes the buying horizon for one product in Europe.
Inventory goes down. Service stays flat. The result looks better.
So the team keeps the change.
Then the supplier minimum is missed.
The U.S. scope has to buy extra units to recover the minimum. Those units sit for months. The vendor order moves to a different week. Upstream inventory changes. Downstream replenishment changes. A local improvement quietly makes the total system worse.
This is not an edge case. It is what happens whenever decisions share constraints, resources, or future state.
The mistake is simple:
You measured the effect where you made the change instead of where the change actually propagated.
In real supply chains, the unit you optimize and the unit you score are often not the same thing.
Frame the Coupling Before You Choose the Algorithm
Suppose each product has:
- an upstream buying horizon,
- a downstream buying horizon,
- one version of each horizon by geographic scope,
- shared supplier minimums across scopes,
- shared vendor capacity,
- different demand patterns by scope,
- inventory that moves through multiple echelons.
A product in the U.S., Europe, and Japan may therefore have six policy parameters:
h_up[product, US] h_down[product, US]
h_up[product, EU] h_down[product, EU]
h_up[product, JP] h_down[product, JP]
If each horizon can take eight values, one product already has:
8^6 = 262,144
possible configurations.
Now add hundreds or thousands of products.
The search space is large, but size is not the first problem. The first problem is coupling.
Before writing a search procedure, ask:
- Which decisions share an MOQ?
- Which decisions share capacity?
- Which decisions change the same order event?
- Which decisions affect the same inventory state?
- Which scopes can compensate for each other?
- Which products are substitutes?
- Which decisions change future feasible decisions?
- At what level is the economic objective actually measured?
If changing one variable can alter the value of another variable, you do not have independent tuning problems.
You have a coupled decision system.
The Objective Must Live at the Level of the Consequence
Suppose the total economic score is:
J(H) =
holding_cost(H)
+ stockout_cost(H)
+ ordering_cost(H)
+ transfer_cost(H)
+ MOQ_penalty(H)
+ capacity_penalty(H)
where H is the complete matrix of upstream and downstream horizons across products and scopes.
The important part is not the exact formula.
The important part is that J scores the full system.
A local score such as:
J[product, EU](H)
may be useful for diagnosis. It is not necessarily a valid acceptance criterion.
If the EU choice changes a shared vendor order, the consequence belongs partly to other scopes. If it changes upstream timing, the consequence may appear several periods later. If it changes which MOQ is triggered, the effect may land on completely different products.
This leads to a practical rule:
Use local metrics to explain changes. Use the coupled objective to accept changes.
Why “Optimize One Scope First” Can Be Wrong
A common decomposition is:
1. Optimize US.
2. Fix US.
3. Optimize EU.
4. Fix EU.
5. Optimize JP.
This is attractive because it is simple.
It is also order-dependent.
The first scope gets the best choices while later scopes inherit the consequences. If scopes share an MOQ, the first scope may choose a horizon that looks efficient alone but leaves an awkward residual requirement for everyone else.
Reverse the order and you may get a different answer.
That is a warning sign.
A decomposition is not wrong because it solves pieces. Almost every large optimization method solves pieces somehow. It is wrong when it pretends the pieces are independent.
The same issue appears with products:
1. Optimize product A.
2. Fix it.
3. Optimize product B.
4. Fix it.
If A and B share a vendor minimum, product A’s “optimal” decision depends on what product B eventually does.
Fixing A too early can destroy the better joint solution.
The Decision Structure
A useful way to write the problem is:
state S = {
inventory,
pipeline,
open_orders,
forecasts,
vendor_rules,
MOQ_status,
capacity,
calendars
}
decision H = {
upstream_horizon[product, scope],
downstream_horizon[product, scope]
}
uncertainty W = {
demand_paths,
lead_times,
disruptions,
forecast_updates
}
The policy may be a direct lookup:
H = fixed matrix of horizon choices
or a state-dependent rule:
H_t = pi(S_t; theta)
where theta contains tunable policy parameters.
That distinction matters.
If horizons are reselected as the state changes, you are tuning a policy. If horizons are fixed for a long planning period, you are choosing a configuration. The evaluation machinery may look similar, but the deployment behavior is different.
The Right Acceptance Test
Suppose you propose changing one horizon:
h_up[item_42, EU]: 5 -> 6
Do not ask only:
Did item 42 in Europe improve?
Ask:
Did the total system improve after all dependent consequences were recomputed?
The basic move is:
H_candidate = copy(H_current)
H_candidate[item_42, EU, upstream] = 6
score_current = evaluate(H_current)
score_candidate = evaluate(H_candidate)
if score_candidate < score_current:
accept
else:
reject
The evaluation must rebuild or update every consequence affected by that move.
That may include:
- vendor order consolidation,
- MOQ feasibility,
- upstream purchase timing,
- downstream replenishment,
- shared capacity,
- inventory trajectories,
- shortage exposure.
If you change one cell but score only one cell, you are not doing local search. You are doing local accounting.
Start With Coordinate Search, But Score Globally
For a large discrete horizon problem, a practical baseline is coordinate search.
Start with a feasible matrix H.
For each product-scope-horizon coordinate:
for coordinate in coordinates:
for value in allowed_values:
candidate = H with coordinate changed to value
score = evaluate_full_system(candidate)
keep the best improving value
Repeat until no meaningful improvement remains.
This is not glamorous. It is useful.
It gives you:
- a working baseline,
- an interpretable move structure,
- a way to measure interaction strength,
- a benchmark for more sophisticated methods.
But naive coordinate search can be expensive. If every move triggers a full simulation of the entire network, runtime explodes.
The next question is not immediately “Which fancy optimizer should we use?”
The next question is:
What actually changed?
Recompute the Dependency Neighborhood
A change to one product-scope horizon usually does not affect the entire supply chain.
It may affect:
changed product-scope
↓
shared product across scopes
↓
shared vendor order
↓
other products on that vendor
↓
shared capacity or MOQ event
Call this the dependency neighborhood.
Instead of evaluating the entire world after every move, identify the smallest set of state and decisions that must be recomputed exactly.
For example:
N(move) = {
changed product,
all scopes for that product,
all products sharing affected vendor-order events,
affected time periods
}
Then evaluate:
Delta J = J_affected(candidate) - J_affected(current)
while keeping unaffected contributions cached.
This can make a huge difference.
But the dependency graph must be correct. Missing one coupling creates a fast algorithm that accepts bad moves.
Speed does not rescue an invalid neighborhood.
Block Moves Are Often Better Than Single Moves
Coordinate search struggles when two changes are bad individually but good together.
Suppose:
Change EU horizon alone: +$20,000 cost
Change JP horizon alone: +$15,000 cost
Change both together: -$80,000 cost
Why?
Together they may move enough volume to satisfy an MOQ efficiently. Individually they may create awkward partial orders.
A single-coordinate method rejects both moves and gets stuck.
That is when block moves matter.
Useful blocks include:
- all scopes for one product,
- all products for one vendor,
- upstream and downstream horizons for one product-scope,
- all decisions tied to one order event,
- all products consuming one constrained resource.
A practical search can mix move sizes:
1. Single-coordinate moves for cheap improvement.
2. Product blocks to coordinate scopes.
3. Vendor blocks to handle shared MOQs.
4. Occasional larger perturbations to escape local minima.
The block structure should follow the business coupling, not arbitrary array dimensions.
Product-First or Vendor-First Is the Wrong Question
Teams often debate the hierarchy:
Should we optimize by product first, then scope?
or:
Should we optimize by vendor first, then product?
There is no universally correct ordering.
The right question is:
Where is the strongest coupling?
If the dominant constraint is a shared vendor MOQ, vendor blocks deserve priority.
If upstream and downstream horizons for the same product strongly interact, product blocks deserve priority.
If regional capacities are binding, scope blocks may matter most.
The search hierarchy should be discovered from the economics and constraints.
Do not organize the algorithm around the database schema just because the data happens to be stored as vendor, product, scope.
Measure Interaction Instead of Guessing It
You can estimate whether two decisions interact.
Let:
Delta_i = J(H with move i) - J(H)
Delta_j = J(H with move j) - J(H)
Delta_ij = J(H with both moves) - J(H)
An interaction measure is:
I(i,j) = Delta_ij - Delta_i - Delta_j
If I(i,j) is near zero, the moves are approximately additive.
If it is large, the moves interact.
This is useful for designing blocks.
You do not need to compute every pair. Sample moves and focus on likely couplings:
- same vendor,
- same product across scopes,
- same scope and capacity pool,
- adjacent echelons,
- same order calendar.
The result can become an interaction graph. Strongly connected decisions belong in the same neighborhood more often.
Now decomposition is based on evidence.
Uncertainty Makes Local Improvements Even More Dangerous
Under uncertainty, one deterministic scenario can make almost any policy look smart.
A horizon change may reduce cost under the mean forecast while increasing exposure to tail demand. A longer upstream horizon may look expensive in normal weeks but valuable during supplier delay. A shorter downstream horizon may reduce average inventory while increasing repeated stockouts.
Evaluate candidates across a scenario bank:
J(H) = E[C(H, W)]
or, when downside matters:
J(H) = E[C(H, W)] + lambda * CVaR_alpha(C(H, W))
Use the same scenarios when comparing nearby candidates.
Common random numbers matter because the decision difference may be much smaller than the simulation noise.
If candidate A sees easy demand paths and candidate B sees hard ones, the search is partly optimizing random seeds.
For every candidate comparison, track:
- mean economic cost,
- standard error of the difference,
- service impact,
- tail cost,
- stockout frequency,
- MOQ violations,
- capacity violations.
Do not accept a $5,000 simulated improvement with $100,000 of comparison noise.
Hard Constraints, Soft Constraints, and Repair
A candidate horizon matrix can fail operationally.
Some violations should make the candidate infeasible:
- impossible supplier calendars,
- hard capacity limits,
- forbidden sourcing relationships,
- legal or contractual restrictions.
Other violations may be economically penalized:
- target service misses,
- overtime,
- expedite usage,
- soft inventory limits.
Be explicit.
A common failure mode is hiding every difficult rule inside a giant penalty function. The search then spends most of its time exploring nonsense.
Another failure mode is over-repairing candidates.
Suppose a proposed move violates an MOQ, so a repair heuristic adds units to other products. The repaired candidate may be feasible, but now you are evaluating the move plus the repair policy.
That is not automatically wrong. It just needs to be understood.
Track:
proposed move
repair actions
final implemented decision
economic consequence
Otherwise the search becomes impossible to debug.
When a MILP Helps
If the important relationships can be represented explicitly, a MILP can generate coordinated candidates.
For discrete horizon choices, define:
x[p,s,u,d] = 1
if product p in scope s uses upstream horizon u and downstream horizon d.
Choose exactly one pair:
sum(u,d) x[p,s,u,d] = 1
Then connect choices to approximated economics, MOQ logic, capacity, or vendor activation.
The MILP does not need to replace the simulator.
A useful hybrid is:
1. Use a MILP or structured heuristic to generate feasible coordinated candidates.
2. Evaluate candidates in the full stochastic simulator.
3. Learn where the approximation is wrong.
4. Generate better candidates.
This is often more practical than forcing every stochastic operational detail into one enormous mathematical model.
The optimization model proposes. The simulator evaluates consequences the model does not represent well.
Metrics for the Search Process
Do not track only the final objective.
Track the behavior of the search itself:
- incumbent objective by iteration,
- best improvement by move type,
- accepted moves by block type,
- candidate evaluations,
- simulation replications,
- time per candidate,
- cache hit rate,
- number of affected entities per move,
- repair frequency,
- infeasible candidate rate,
- improvement versus compute budget,
- variance of candidate differences,
- sensitivity to starting solution,
- sensitivity to search order.
Search-order sensitivity is especially useful.
Run the same method with different coordinate orders. If results vary wildly, the landscape is strongly coupled or the method is fixing decisions too aggressively.
That does not mean the algorithm is useless. It tells you where to improve it.
Implementation Notes That Matter
Keep the full decision matrix explicit
Do not scatter policy values across hidden defaults, configuration files, and mutable objects.
You should be able to print the complete candidate policy.
Separate proposal from evaluation
The search algorithm should propose a candidate. The evaluator should score it.
Do not let the simulator quietly change the candidate unless repair is explicit and logged.
Version the scenario bank
Candidate comparisons must be reproducible.
Store scenario seeds, forecast vintages, lead-time assumptions, and disruption models.
Cache by dependency
Cache expensive structures that truly do not change. Invalidate only what the move affects.
Log the delta decomposition
For every accepted move, record:
holding cost -$120,000
stockout cost +$30,000
ordering cost +$10,000
MOQ consequence -$70,000
----------------------------
total improvement -$150,000
A search process that cannot explain its own accepted moves will eventually lose trust.
Keep an untouched final evaluation set
Do not use the same finite scenario bank forever for search and final reporting.
The search can overfit simulation scenarios just like a machine learning model can overfit training data.
Use a holdout scenario set for final policy comparison.
Common Failure Modes
Scoring only the changed scope
The move looks good locally and bad globally.
Fixing decisions too early
Early choices consume shared resources and force later choices into bad residual problems.
Using blocks that follow the org chart
The algorithm optimizes U.S., Europe, and Japan separately because the company is organized that way, even though the MOQ is global.
Recomputing everything
The evaluation is correct but too slow to search meaningfully.
Recomputing too little
The evaluation is fast but misses a real dependency.
Accepting noisy improvements
The search chases simulation variance.
Ignoring paired moves
Single-coordinate search gets stuck because important improvements require coordination.
Hiding repair logic
The candidate you think you evaluated is not the decision the simulator actually executed.
Optimizing a proxy that does not survive deployment
The search minimizes modeled cost while the business cares about service, cash, workload, or execution stability.
What to Do in Practice
Start simple, but do not start naive.
A practical sequence is:
- Write the full decision matrix explicitly.
- Draw the coupling graph: product, scope, vendor, echelon, time, capacity.
- Define one global economic acceptance metric.
- Build a feasible baseline policy.
- Implement single-coordinate moves scored globally.
- Profile which dependencies actually change after each move.
- Add cached incremental evaluation where safe.
- Measure interaction and introduce blocks around strong couplings.
- Use common random numbers for stochastic comparisons.
- Add larger neighborhoods or MILP-generated candidates only after the baseline is understood.
- Validate the final policy on untouched scenarios and operational replay.
The goal is not to find the most sophisticated search algorithm.
The goal is to avoid lying to yourself about improvement.
A supply chain is full of decisions that look independent in a table and interact in reality. The algorithm has to respect the reality.
If a scope shares an MOQ, it is not independent.
If two echelons share state, they are not independent.
If today’s choice changes tomorrow’s feasible actions, the periods are not independent.
And if you change one part of the system but score only that part, the number you call “improvement” may not mean anything at all.