How IReducer Enables Early Termination in Extension Execution Loops
The IReducer interface allows the ExtensionInvocationHandler to break out of the extension execution loop as soon as its shouldStop() method returns true, preventing unnecessary processing of remaining extensions.
In the funkygao/cp-ddd-framework (CP-DDD) runtime, domain extensions are invoked sequentially via a dynamic proxy. When multiple implementations of an extension point exist, the framework uses an IReducer to determine whether to continue iterating or halt early based on accumulated results. This mechanism is critical for performance-sensitive operations like fail-fast validation or first-match lookups.
The IReducer Contract: shouldStop and reduce
The IReducer<R> interface defines two responsibilities: aggregating final results and signaling early termination. Located in dddplus-runtime/src/main/java/io/github/dddplus/runtime/IReducer.java (lines 22–71), the contract is:
public interface IReducer<R> {
R reduce(List<R> accumulatedResults); // produce the final value
boolean shouldStop(List<R> accumulatedResults); // true → break the loop
}
reduceis called after the loop finishes (or breaks) to transform the list of intermediate results into a single return value.shouldStopis consulted after each extension execution. If it returnstrue, the loop inExtensionInvocationHandlerterminates immediately, and no further extensions are invoked.
Where Early Termination Happens: ExtensionInvocationHandler
The actual execution loop resides in ExtensionInvocationHandler.java (lines 64–88). After each extension is invoked via reflection, its result is appended to accumulatedResults. The code then checks the reducer:
// Inside ExtensionInvocationHandler.invoke()
for (Object ext : extensions) {
// ... invoke extension ...
R result = (R) method.invoke(ext, args);
accumulatedResults.add(result);
if (reducer == null || reducer.shouldStop(accumulatedResults)) {
break; // ← Early termination point
}
}
If reducer is null, the framework defaults to running all extensions. When a reducer is provided and shouldStop returns true, the break statement exits the for-each loop, skipping any remaining extension implementations.
Built-in Reducers That Enable Early Exit
CP-DDD provides several static factory methods in IReducer.java for common termination strategies.
stopOnFirstMatch
IReducer.stopOnFirstMatch(Predicate<R>) is the primary mechanism for early exit. It returns the last accumulated result (the "tail") and stops as soon as that tail satisfies the predicate.
Implementation (lines 50–71):
public static <R> IReducer<R> stopOnFirstMatch(Predicate<R> predicate) {
return new IReducer<R>() {
@Override
public R reduce(List<R> accumulatedResults) {
return tail(accumulatedResults); // return last element
}
@Override
public boolean shouldStop(List<R> accumulatedResults) {
R tail = tail(accumulatedResults);
return tail != null && predicate.test(tail); // stop when predicate matches
}
};
}
allOf (Non-terminating)
IReducer.allOf() is the opposite: it aggregates all results and never triggers early termination. Its shouldStop always returns false, ensuring every extension implementation executes.
public static <R> IReducer<R> allOf() {
return new IReducer<R>() {
@Override
public R reduce(List<R> accumulatedResults) { return null; }
@Override
public boolean shouldStop(List<R> accumulatedResults) { return false; }
};
}
Real-World Scenarios for Early Termination
The IReducer pattern enables several domain-driven design patterns where short-circuiting extension execution improves performance and correctness.
Fail-Fast Validation
In order management, multiple extensions may check if an order is eligible for shipping. If any extension returns false, the operation should abort immediately.
From OrderAllowShipExtRouter.java (lines 16–22):
public Boolean allowShipping(Order order) {
Predicate<Boolean> stopper = allow -> Boolean.FALSE.equals(allow);
// Stop as soon as any extension returns false
Boolean allow = forEachExtension(order, IReducer.stopOnFirstMatch(stopper))
.execute(order);
return allow != null ? allow : Boolean.TRUE;
}
Here, IReducer.stopOnFirstMatch enables early termination the moment a single extension vetoes the shipment, preventing unnecessary processing of subsequent policies.
First-Match Lookup
When selecting a shipping carrier, you might have multiple extensions each claiming they can handle the order. You only need the first one that returns a non-null carrier.
Predicate<Carrier> firstAvailable = carrier -> carrier != null;
Carrier carrier = forEachExtension(order, IReducer.stopOnFirstMatch(firstAvailable))
.execute(order);
The loop terminates as soon as the first extension returns a valid Carrier object, avoiding redundant invocations.
Custom Threshold Logic
You can implement domain-specific aggregation rules, such as "stop after three successful validations" or "stop when the cumulative risk score exceeds 100."
class RiskThresholdReducer implements IReducer<Integer> {
private final int threshold;
public RiskThresholdReducer(int threshold) { this.threshold = threshold; }
@Override
public Integer reduce(List<Integer> results) {
return results.stream().mapToInt(i -> i).sum();
}
@Override
public boolean shouldStop(List<Integer> results) {
return results.stream().mapToInt(i -> i).sum() >= threshold;
}
}
// Usage
int risk = forEachExtension(context, new RiskThresholdReducer(100))
.calculateRisk(context);
Summary
IReduceris the control mechanism that allows the CP-DDD framework to terminate extension execution loops early via itsshouldStop(List<R>)method.- Early termination occurs in
ExtensionInvocationHandler.java(lines 64–88) whenreducer.shouldStop(accumulatedResults)returnstrue, triggering abreakstatement. IReducer.stopOnFirstMatch(Predicate)is the built-in implementation for fail-fast scenarios, stopping the loop when the predicate matches the latest result.IReducer.allOf()never stops early, ensuring all extensions execute for full aggregation.- Real-world applications include fail-fast validation (e.g.,
OrderAllowShipExtRouter), first-match lookups, and custom threshold logic.
Frequently Asked Questions
What is the difference between reduce() and shouldStop() in IReducer?
The reduce() method aggregates the final list of results into a single return value after the loop finishes or breaks. The shouldStop() method is checked after every extension execution to determine whether to break the loop early. While reduce() handles post-processing, shouldStop() controls runtime flow.
How does stopOnFirstMatch work internally?
stopOnFirstMatch creates an anonymous IReducer implementation whose shouldStop method retrieves the last element (tail) of accumulatedResults and tests it against the provided predicate. If the predicate returns true, shouldStop returns true, causing ExtensionInvocationHandler to break the loop. The reduce method simply returns that tail element.
Can I implement custom early termination logic?
Yes. You can implement the IReducer interface directly to define domain-specific stop conditions. For example, you might stop after a specific number of successful results, when a cumulative score exceeds a threshold, or when a particular pattern emerges across multiple extension results. Pass your custom instance to BaseRouter.forEachExtension().
Where is the extension execution loop located?
The loop resides in ExtensionInvocationHandler.invoke() within the dddplus-runtime module, specifically lines 64–88. This class acts as a dynamic proxy that iterates over all registered extension implementations, invokes the method via reflection, and checks the IReducer.shouldStop() condition after each invocation to determine whether to continue or break.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →