Implementing Custom IReducer to Control Extension Execution Flow in BaseRouter: A Complete Guide
To control extension execution flow in BaseRouter, implement the IReducer interface with custom shouldStop() and reduce() methods, or use built-in factories like stopOnFirstMatch() to determine when iteration halts and how results aggregate.
The cp-ddd-framework (funkygao/cp-ddd-framework) uses BaseRouter as the core abstraction for routing domain logic to extension points. By supplying a custom IReducer to the forEachExtension method, you dictate exactly how multiple extensions execute and how their results combine into a single return value.
How BaseRouter Delegates Execution to IReducer
In BaseRouter.java (lines 44–59), the forEachExtension method accepts an IReducer<R> parameter that controls the entire execution lifecycle:
protected <R> Ext forEachExtension(@NonNull Identity identity,
@NonNull IReducer<R> reducer) {
return forEachExtension(identity, 0, reducer);
}
The actual iteration logic resides in ExtensionInvocationHandler.java (lines 81–95). The handler walks through all effective extensions and consults the reducer after each invocation:
for (ExtensionDef extensionDef : effectiveExts) {
result = invokeExtension(extensionDef, method, args);
accumulatedResults.add(result);
if (reducer == null || reducer.shouldStop(accumulatedResults)) {
break; // stop condition controlled by reducer
}
}
return reducer != null ? reducer.reduce(accumulatedResults) : result;
This two-phase approach—accumulate then decide—means your custom IReducer determines both the early-termination policy via shouldStop() and the final aggregation logic via reduce().
Built-in IReducer Factory Methods
The IReducer.java interface (lines 50–71 and 88–99) provides static factories for common execution patterns.
Stop on First Match
IReducer.stopOnFirstMatch(Predicate<R>) halts execution as soon as the last extension result matches the supplied predicate, returning that specific result. If no match occurs, it returns the final extension’s result.
Use case: Fail-fast validation pipelines where the first false or error response should abort further processing.
Run All Extensions (No Aggregation)
IReducer.allOf() never triggers early termination; its reduce() implementation always returns null. Consequently, BaseRouter returns the raw result of the last executed extension, effectively ignoring aggregation.
Use case: Scenarios where side effects matter but only the final extension’s return value is relevant.
Legacy First Match (Deprecated)
IReducer.allOf(Predicate<R>) executes all extensions unconditionally but returns only the first result satisfying the predicate. This API is deprecated; prefer stopOnFirstMatch for new implementations.
Custom IReducer Implementation Strategies
When built-in factories lack the specificity your domain requires, implement IReducer directly to define custom stopping conditions and reduction algorithms.
Strategy 1: Aggregate All Results into a Collection
Implement a reducer that flattens multiple list outputs into a single unified collection by never stopping early and combining results in reduce():
IReducer<List<String>> collectAll = new IReducer<>() {
@Override
public List<String> reduce(List<List<String>> accumulatedResults) {
// Flatten the list-of-lists produced by each extension
return accumulatedResults.stream()
.flatMap(List::stream)
.collect(Collectors.toList());
}
@Override
public boolean shouldStop(List<List<String>> accumulatedResults) {
// Execute every available extension
return false;
}
};
Application: Extensions each return partial identifier lists; the router merges them into a comprehensive result set.
Strategy 2: Stop on First Error
Leverage the built-in factory for boolean pipelines where false indicates failure:
IReducer<Boolean> stopOnError = IReducer.stopOnFirstMatch(result -> !result);
If any extension returns false (error), iteration stops immediately and that value propagates upward. If all return true, the final true is returned.
Application: Multi-layer permission checks or validation rules where the first denial should prevent subsequent evaluations.
Strategy 3: Compute Maximum Value Across Extensions
Create a reducer that collects all numeric priorities and selects the highest value:
IReducer<Integer> maxReducer = new IReducer<>() {
@Override
public Integer reduce(List<Integer> accumulatedResults) {
return accumulatedResults.stream()
.max(Integer::compareTo)
.orElse(null);
}
@Override
public boolean shouldStop(List<Integer> accumulatedResults) {
// All extensions must execute to determine the true maximum
return false;
}
};
Application: Auction or bidding extensions where each proposes a priority score, and the router selects the winner with the highest value.
Wiring Custom Reducers into Your Router
Subclass BaseRouter and override extension methods to inject your reducer into the execution flow. The following example from the framework’s routing pattern demonstrates stopping on the first denial:
public class OrderAllowShipRouter extends BaseRouter<OrderAllowShipExt, OrderIdentity> {
@Override
public OrderAllowShipExt defaultExtension(@NonNull OrderIdentity identity) {
return OrderAllowShipExt::allow; // Fallback that always permits
}
public boolean allowShip(Order order) {
// Stop immediately if any extension denies shipment (returns false)
IReducer<Boolean> stopIfDenied = IReducer.stopOnFirstMatch(allowed -> !allowed);
return forEachExtension(new OrderIdentity(order), stopIfDenied);
}
}
According to the source code in dddplus-runtime, this pattern appears in concrete routers like OrderAllowShipExtRouter, which use stopOnFirstMatch to implement circuit-breaker logic across multiple shipping policy extensions.
Summary
- BaseRouter delegates iteration control to
ExtensionInvocationHandler, which respectsIReducer.shouldStop()for early termination andIReducer.reduce()for final result calculation. - Built-in factories (
stopOnFirstMatch,allOf) handle common patterns like fail-fast validation or last-value-wins execution without custom code. - Custom implementations require only two methods:
shouldStop()to determine when to break the loop, andreduce()to aggregate accumulated results. - File references:
BaseRouter.java(lines 44–59),ExtensionInvocationHandler.java(lines 81–95), andIReducer.java(lines 50–71, 88–99) contain the core implementation details.
Frequently Asked Questions
What interface methods must I implement for a custom IReducer?
You must implement R reduce(List<R> accumulatedResults) to define final aggregation logic, and boolean shouldStop(List<R> accumulatedResults) to determine whether the extension iteration should halt early. Both methods receive the growing list of results from preceding extension calls.
How does stopOnFirstMatch differ from the deprecated allOf(Predicate)?
stopOnFirstMatch(Predicate<R>) stops execution immediately when a result matches the predicate, returning that matching value. The deprecated allOf(Predicate<R>) executes all extensions regardless of matches, then returns the first matching result from the complete list. The modern API is more efficient for fail-fast scenarios.
Can I use lambda expressions instead of anonymous classes for IReducer?
Yes, because IReducer is a functional interface with two abstract methods. However, since Java functional interfaces require exactly one abstract method, you cannot use a plain lambda for IReducer directly. Instead, use the static factory methods like IReducer.stopOnFirstMatch() which return pre-built instances, or provide an anonymous class for custom two-method implementations.
Why does BaseRouter return the last extension result when IReducer.reduce returns null?
When reduce() returns null, ExtensionInvocationHandler returns the variable result, which holds the return value of the most recently executed extension. This behavior ensures that IReducer.allOf()—which intentionally returns null from reduce()—naturally yields the last extension’s raw output without additional conditional logic in the handler.
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 →