How to Implement an IDomainExtension with Multiple Routing Modes in cp-ddd-framework
Implement an IDomainExtension by defining an interface that extends IDomainExtension, annotating concrete Spring beans with @Extension, extending BaseRouter to handle selection logic, and choosing between all-match, first-match, or predicate-stop execution strategies via the router's API.
The cp-ddd-framework provides a robust extension mechanism that allows business logic to vary based on identity patterns, partners, or policies. An IDomainExtension serves as the core extension-point interface, and by leveraging the BaseRouter class, you can implement extensions that support multiple routing modes—from executing all matching implementations to selecting specific ones based on runtime conditions.
Understanding the Three Routing Modes
The BaseRouter class in dddplus-runtime/src/main/java/io/github/dddplus/runtime/BaseRouter.java provides three distinct routing strategies for invoking extensions. Each mode serves different architectural needs:
-
All-match (
forEachExtension(identity)): Executes all matching extension implementations without stopping. Use this mode for side effects, logging, or accumulating results across multiple extensions. -
First-match (
firstExtension(identity)): Returns the first matching implementation and executes it exclusively. This mode enforces mutual exclusion when you need exactly one extension to handle a specific identity. -
Predicate-stop (
forEachExtension(identity, IReducer.stopOnFirstMatch(predicate))): Executes implementations sequentially until a predicate condition is satisfied, then stops immediately. This "fail-fast" mode is ideal for validation checks or permission gates.
According to the cp-ddd-framework source code, the routing logic is handled by ExtensionInvocationHandler, which iterates over indexed ExtensionDef instances and applies the supplied IReducer to determine when to stop execution.
Step-by-Step Implementation
1. Define the Extension Interface
Create an interface that extends IDomainExtension. All methods must return wrapper types (not primitives) or void, as enforced by ExtensionMethodSignatureEnforcer.
// src/main/java/com/example/ext/IGreetingExt.java
import io.github.dddplus.ext.IDomainExtension;
public interface IGreetingExt extends IDomainExtension {
/** Returns a greeting for the given user name. */
String greet(String userName);
}
Reference the interface definition in dddplus-spec/src/main/java/io/github/dddplus/ext/IDomainExtension.java.
2. Annotate Concrete Implementations
Create Spring beans annotated with @Extension. The code attribute must correspond to a pattern, partner, or policy code defined elsewhere in your application (e.g., FooPattern.CODE).
// src/main/java/com/example/ext/EnglishGreetingExt.java
import io.github.dddplus.annotation.Extension;
@Extension(code = FooPattern.CODE, name = "English greeting")
public class EnglishGreetingExt implements IGreetingExt {
@Override
public String greet(String userName) {
return "Hello, " + userName;
}
}
// src/main/java/com/example/ext/ChineseGreetingExt.java
@Extension(code = FooPattern.CODE, name = "Chinese greeting")
public class ChineseGreetingExt implements IGreetingExt {
@Override
public String greet(String userName) {
return "你好," + userName;
}
}
The @Extension annotation inherits from @Component, so Spring automatically discovers these beans at startup. See the annotation definition in dddplus-runtime/src/main/java/io/github/dddplus/annotation/Extension.java.
3. Configure the Default Extension
Provide a fallback implementation that executes when no specific extension matches the identity. Create a no-op bean or a sensible default and reference it in your router.
// Default no-op implementation
@Extension(code = IGreetingExt.DefaultCode)
public class DefaultGreetingExt implements IGreetingExt {
@Override
public String greet(String userName) {
return null; // Safe because String is a wrapper type
}
}
4. Extend BaseRouter
Create an abstract router class that extends BaseRouter<YourExtensionType, YourIdentityType>. Implement the defaultExtension(Identity) method to return your fallback bean.
// src/main/java/com/example/router/GreetingRouter.java
import io.github.dddplus.runtime.BaseRouter;
import io.github.dddplus.runtime.DDD;
public abstract class GreetingRouter
extends BaseRouter<IGreetingExt, UserIdentity> {
@Override
public IGreetingExt defaultExtension(@NonNull UserIdentity identity) {
// Retrieve the default extension via DDD.usePolicy or direct injection
return DDD.usePolicy(DefaultGreetingPolicy.class, identity);
}
}
The router resolves the extension class (extClazz) via InternalIndexer.getBaseRouterExtDeclaration(...), defined in dddplus-runtime/src/main/java/io/github/dddplus/runtime/registry/InternalIndexer.java.
5. Execute with Different Routing Strategies
Use the router in your service layer according to the required routing mode.
All-match execution (side effects only):
public void logAllGreetings(String userName, UserIdentity identity) {
GreetingRouter router = DDD.usePolicy(GreetingPolicy.class, identity);
router.forEachExtension(identity)
.greet(userName); // All matching extensions execute
}
First-match execution (mutually exclusive):
public String getLocalizedGreeting(String userName, UserIdentity identity) {
GreetingRouter router = DDD.usePolicy(GreetingPolicy.class, identity);
IGreetingExt ext = router.firstExtension(identity);
return ext.greet(userName); // Only the first matching extension runs
}
Predicate-stop execution (fail-fast):
public String findFirstNonNullGreeting(String userName, UserIdentity identity) {
GreetingRouter router = DDD.usePolicy(GreetingPolicy.class, identity);
return router.forEachExtension(identity,
IReducer.stopOnFirstMatch(msg -> msg != null))
.greet(userName);
}
The IReducer utilities are defined in dddplus-runtime/src/main/java/io/github/dddplus/runtime/IReducer.java.
Extension Discovery and Routing Internals
The framework indexes all extensions at startup through InternalIndexer, which builds maps (sortedPatternMap, policyDefMap, partnerDefMap) keyed by the generic extension class. When you invoke a router method, BaseRouter.findExtension creates an ExtensionInvocationHandler that iterates over the indexed ExtensionDef instances.
The proxy invokes each bean dynamically and applies the IReducer to determine whether to continue iteration. If no concrete extension matches the identity, the router falls back to the defaultExtension implementation. Because extension methods must return wrapper types or void, returning null from a default implementation is type-safe and will not cause primitive unboxing errors.
Summary
- Define an interface extending
IDomainExtensionwith wrapper return types only. - Annotate concrete implementations with
@Extension(code = "...")to enable Spring discovery and indexing. - Extend
BaseRouterand implementdefaultExtension()to provide a safe fallback. - Choose
forEachExtension()for all-match scenarios,firstExtension()for exclusive selection, orforEachExtension(identity, IReducer)for predicate-based stopping. - Reference specific source files (
BaseRouter.java,InternalIndexer.java,ExtensionInvocationHandler.java) to understand the routing pipeline.
Frequently Asked Questions
What happens if no extension matches the identity and no default is provided?
The router returns null from firstExtension(), which will cause a NullPointerException if you attempt to invoke methods on it. Always implement defaultExtension() in your router class to return a no-op implementation or throw a meaningful business exception.
Can I use primitive types in IDomainExtension method signatures?
No. The framework enforces that all extension methods must return wrapper types (e.g., Integer instead of int) or void. This requirement is validated by ExtensionMethodSignatureEnforcer to ensure that default extensions can safely return null without causing unboxing failures.
How do I implement custom aggregation logic beyond the built-in reducers?
Implement the IReducer interface with custom reduce() and shouldStop() methods. Pass your implementation to forEachExtension(identity, customReducer) to control how results are accumulated and when the iteration should terminate. This approach supports complex scenarios like collecting partial results from multiple extensions until a specific threshold is reached.
Why does BaseRouter require an abstract class rather than a concrete one?
BaseRouter uses generics to bind the extension type and identity type, and it relies on Spring's component scanning to instantiate the actual router beans. By declaring your router as an abstract class extending BaseRouter, you allow the framework to generate a concrete proxy that handles the ExtensionInvocationHandler logic while you provide only the defaultExtension() configuration.
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 →