Skip to content

Commit ce4cf96

Browse files
vanroguclaude
andauthored
Add adapter registry pattern for dependency injection in bounded contexts (#45)
* Replace preConfigure callbacks with port/adapter pattern Introduce a declarative port/adapter mechanism for wiring infrastructure dependencies into feature slices, replacing the preConfigure callback. API (sliceworkz-eventmodeling-api): - Add adapter(Object)/port(Class)/port(Class, String) to BoundedContextBuilder - Add AdapterBinding and QualifiableAdapterBinding for fluent chaining with optional .withQualification("name") for named bindings - Remove preConfigure from FeaturesSpecification Implementation (sliceworkz-eventmodeling-impl): - Add AdapterRegistry with register/lookup/requalify operations - Assignability check: adapter must implement the port type - Fail-fast: missing port lookup throws IllegalStateException at build time - Duplicate detection: registering same port+qualification twice throws - DelegatingBoundedContextBuilder enables QualifiableAdapterBinding to continue the builder chain fluently Benchmark refactoring: - OrderProcessingFeatureSlice is now a clean marker interface (no preConfigure) - Feature slices use builder.port(DataSource.class) and builder.port(DatabaseInitMode.class) in their configure methods - BenchmarkApplication uses .adapter(ds).forPort(DataSource.class) and .adapter(initMode).forPort(DatabaseInitMode.class) - Eliminated mutable fields and two-phase initialization from all slices https://claude.ai/code/session_01L4BtP5BGKBqwTe3rYphCK1 * Simplify adapter binding: forPort() returns BoundedContextBuilder directly Remove QualifiableAdapterBinding and DelegatingBoundedContextBuilder. Follow the same pattern as FeaturesSpecification.done() and LiveModelSpecification.live() where the terminal method returns the builder directly. AdapterBinding.forPort(Class) now returns BoundedContextBuilder, and an overload forPort(Class, String) handles qualified bindings. This also removes the requalify() method from AdapterRegistry since qualification is now provided upfront. https://claude.ai/code/session_01L4BtP5BGKBqwTe3rYphCK1 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent e7de359 commit ce4cf96

File tree

13 files changed

+256
-94
lines changed

13 files changed

+256
-94
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Sliceworkz Event Modeling - an opinionated Event Modeling framework in Java
3+
* Copyright © 2025-2026 Sliceworkz / XTi (info@sliceworkz.org)
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Lesser General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Lesser General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Lesser General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
package org.sliceworkz.eventmodeling.boundedcontext;
19+
20+
/**
21+
* Intermediate builder step for binding an adapter to a port.
22+
* <p>
23+
* Created by {@link BoundedContextBuilder#adapter(Object)} and completed
24+
* by calling {@link #forPort(Class)}.
25+
*
26+
* @param <DOMAIN_EVENT_TYPE> the domain event root type
27+
* @param <INBOUND_EVENT_TYPE> the inbound event root type
28+
* @param <OUTBOUND_EVENT_TYPE> the outbound event root type
29+
*/
30+
public interface AdapterBinding<DOMAIN_EVENT_TYPE, INBOUND_EVENT_TYPE, OUTBOUND_EVENT_TYPE> {
31+
32+
/**
33+
* Binds the adapter to the specified port type with the default (unqualified) qualification.
34+
*
35+
* @param <T> the port type
36+
* @param portType the port interface class that the adapter implements
37+
* @return the builder for continued chaining
38+
* @throws IllegalArgumentException if the adapter is not assignable to the port type
39+
*/
40+
<T> BoundedContextBuilder<DOMAIN_EVENT_TYPE, INBOUND_EVENT_TYPE, OUTBOUND_EVENT_TYPE> forPort(Class<T> portType);
41+
42+
/**
43+
* Binds the adapter to the specified port type with a named qualification.
44+
* <p>
45+
* This allows multiple adapters to be registered for the same port type
46+
* under different qualifications.
47+
*
48+
* @param <T> the port type
49+
* @param portType the port interface class that the adapter implements
50+
* @param qualification the qualification name
51+
* @return the builder for continued chaining
52+
* @throws IllegalArgumentException if the adapter is not assignable to the port type
53+
*/
54+
<T> BoundedContextBuilder<DOMAIN_EVENT_TYPE, INBOUND_EVENT_TYPE, OUTBOUND_EVENT_TYPE> forPort(Class<T> portType, String qualification);
55+
56+
}

sliceworkz-eventmodeling-api/src/main/java/org/sliceworkz/eventmodeling/boundedcontext/BoundedContextBuilder.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,44 @@ BoundedContextBuilder<DOMAIN_EVENT_TYPE, INBOUND_EVENT_TYPE, OUTBOUND_EVENT_TYPE
7373
BoundedContextBuilder<DOMAIN_EVENT_TYPE, INBOUND_EVENT_TYPE, OUTBOUND_EVENT_TYPE> dispatcher(
7474
Class<? extends Dispatcher<? extends OUTBOUND_EVENT_TYPE>> dispatcherClass);
7575

76+
/**
77+
* Starts an adapter binding for the given adapter instance.
78+
* <p>
79+
* The returned {@link AdapterBinding} must be completed by calling
80+
* {@link AdapterBinding#forPort(Class)} to specify the port type.
81+
* <p>
82+
* Example:
83+
* <pre>
84+
* .adapter(myDataSource).forPort(DataSource.class)
85+
* .adapter(myCache).forPort(Cache.class, "customers")
86+
* </pre>
87+
*
88+
* @param adapter the adapter instance implementing a port
89+
* @return an adapter binding to specify the port type
90+
*/
91+
AdapterBinding<DOMAIN_EVENT_TYPE, INBOUND_EVENT_TYPE, OUTBOUND_EVENT_TYPE> adapter(Object adapter);
92+
93+
/**
94+
* Retrieves the adapter registered for the given port type with the default qualification.
95+
*
96+
* @param <T> the port type
97+
* @param portType the port interface class
98+
* @return the adapter instance, cast to the port type
99+
* @throws IllegalStateException if no adapter is registered for this port
100+
*/
101+
<T> T port(Class<T> portType);
102+
103+
/**
104+
* Retrieves the adapter registered for the given port type and qualification.
105+
*
106+
* @param <T> the port type
107+
* @param portType the port interface class
108+
* @param qualification the qualification name
109+
* @return the adapter instance, cast to the port type
110+
* @throws IllegalStateException if no adapter is registered for this port and qualification
111+
*/
112+
<T> T port(Class<T> portType, String qualification);
113+
76114
<T extends BoundedContext<DOMAIN_EVENT_TYPE, INBOUND_EVENT_TYPE, OUTBOUND_EVENT_TYPE>> T build( );
77115

78116
<T extends BoundedContext<DOMAIN_EVENT_TYPE, INBOUND_EVENT_TYPE, OUTBOUND_EVENT_TYPE>> T build(Class<T> returnType);

sliceworkz-eventmodeling-api/src/main/java/org/sliceworkz/eventmodeling/boundedcontext/FeaturesSpecification.java

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
*/
1818
package org.sliceworkz.eventmodeling.boundedcontext;
1919

20-
import java.util.function.Consumer;
2120
import java.util.function.Predicate;
2221

2322
import org.sliceworkz.eventmodeling.slices.FeatureSliceConfiguration;
@@ -26,20 +25,6 @@ public interface FeaturesSpecification<DOMAIN_EVENT_TYPE, INBOUND_EVENT_TYPE, OU
2625

2726
FeaturesSpecification<DOMAIN_EVENT_TYPE, INBOUND_EVENT_TYPE, OUTBOUND_EVENT_TYPE> rootPackage(Package rootPackage);
2827

29-
/**
30-
* Registers a pre-configuration callback that will be invoked for each feature slice
31-
* before it is configured.
32-
* <p>
33-
* This allows customization of feature slice configurations before they are built into
34-
* the bounded context. The callback receives the feature slice configuration and can
35-
* modify its settings.
36-
*
37-
* @param preConfigure the callback to invoke for each feature slice configuration
38-
* @return this builder for method chaining
39-
*/
40-
FeaturesSpecification<DOMAIN_EVENT_TYPE, INBOUND_EVENT_TYPE, OUTBOUND_EVENT_TYPE> preConfigure (
41-
Consumer<FeatureSliceConfiguration<DOMAIN_EVENT_TYPE, INBOUND_EVENT_TYPE, OUTBOUND_EVENT_TYPE>> preConfigure );
42-
4328
FeaturesSpecification<DOMAIN_EVENT_TYPE, INBOUND_EVENT_TYPE, OUTBOUND_EVENT_TYPE> filter (Predicate<FeatureSliceConfiguration<DOMAIN_EVENT_TYPE, INBOUND_EVENT_TYPE, OUTBOUND_EVENT_TYPE>> filter);
4429

4530
FeaturesSpecification<DOMAIN_EVENT_TYPE, INBOUND_EVENT_TYPE, OUTBOUND_EVENT_TYPE> enableCommands ( );

sliceworkz-eventmodeling-benchmark/src/main/java/org/sliceworkz/eventmodeling/benchmark/BenchmarkApplication.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,14 +105,13 @@ public static void main ( String[] args ) throws InterruptedException {
105105
.name(BOUNDED_CONTEXT_NAME)
106106
.instance(InstanceFactory.determine(BOUNDED_CONTEXT_NAME))
107107
.eventStorage(eventStorage)
108+
.adapter(dataSource).forPort(DataSource.class)
109+
.adapter(databaseInitMode).forPort(DatabaseInitMode.class)
108110
.features()
109111
.rootPackage(BenchmarkApplication.class.getPackage())
110-
.preConfigure(fs->((OrderProcessingFeatureSlice)fs).preConfigure(dataSource, finalInitializeDatabase))
111112
.done()
112113
.build(OrderProcessingBoundedContext.class);
113114

114-
// bc.<OrderProcessingFeatureSlice>getDeployedFeatureSlices().forEach(fs->fs.postConfigure(bc));
115-
116115
bc.start();
117116

118117
EventStream<Object> domainStream = EventStoreFactory.get().eventStore(eventStorage).getEventStream(EventStreamId.forContext(BOUNDED_CONTEXT_NAME).withPurpose("domain"));

sliceworkz-eventmodeling-benchmark/src/main/java/org/sliceworkz/eventmodeling/benchmark/OrderProcessingFeatureSlice.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717
*/
1818
package org.sliceworkz.eventmodeling.benchmark;
1919

20-
import javax.sql.DataSource;
21-
2220
import org.sliceworkz.eventmodeling.benchmark.OrderProcessingEvent.OrderProcessingDomainEvent;
2321
import org.sliceworkz.eventmodeling.benchmark.OrderProcessingEvent.OrderProcessingInboundEvent;
2422
import org.sliceworkz.eventmodeling.benchmark.OrderProcessingEvent.OrderProcessingOutboundEvent;
@@ -31,6 +29,4 @@ default void configureCommand ( BoundedContextBuilder<OrderProcessingDomainEvent
3129
default void configureQuery ( BoundedContextBuilder<OrderProcessingDomainEvent, OrderProcessingInboundEvent, OrderProcessingOutboundEvent> builder ) { }
3230
default void configureAutomation ( BoundedContextBuilder<OrderProcessingDomainEvent, OrderProcessingInboundEvent, OrderProcessingOutboundEvent> builder ) { }
3331

34-
void preConfigure ( DataSource dataSource, boolean initializeDatabase );
35-
3632
}

sliceworkz-eventmodeling-benchmark/src/main/java/org/sliceworkz/eventmodeling/benchmark/features/announceshipment/AnnounceShipmentFeatureSlice.java

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,25 +26,20 @@
2626
import org.sliceworkz.eventmodeling.boundedcontext.BoundedContextBuilder;
2727
import org.sliceworkz.eventmodeling.slices.FeatureSlice;
2828
import org.sliceworkz.eventmodeling.slices.FeatureSlice.Type;
29+
import org.sliceworkz.eventstore.infra.postgres.DatabaseInitMode;
2930

3031
@FeatureSlice(type=Type.AUTOMATION)
3132
public class AnnounceShipmentFeatureSlice implements OrderProcessingFeatureSlice{
3233

33-
private ShipmentsToBeAnounced shipmentsToBeAnounced;
34-
35-
@Override
36-
public void preConfigure(DataSource dataSource, boolean initializeDatabase) {
37-
this.shipmentsToBeAnounced = new ShipmentsToBeAnounced(dataSource);
38-
if ( initializeDatabase ) {
39-
this.shipmentsToBeAnounced.initialize();
40-
}
41-
}
42-
4334
@Override
4435
public void configureAutomation(
4536
BoundedContextBuilder<OrderProcessingDomainEvent, OrderProcessingInboundEvent, OrderProcessingOutboundEvent> builder) {
37+
var shipmentsToBeAnounced = new ShipmentsToBeAnounced(builder.port(DataSource.class));
38+
if ( builder.port(DatabaseInitMode.class) == DatabaseInitMode.INITIALIZE ) {
39+
shipmentsToBeAnounced.initialize();
40+
}
4641
builder.readmodel(shipmentsToBeAnounced);
4742
builder.automation(new AnnounceShipmentAutomation(shipmentsToBeAnounced));
4843
}
4944

50-
}
45+
}

sliceworkz-eventmodeling-benchmark/src/main/java/org/sliceworkz/eventmodeling/benchmark/features/createshippinglabel/CreateShippingLabelFeatureSlice.java

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,24 +26,18 @@
2626
import org.sliceworkz.eventmodeling.boundedcontext.BoundedContextBuilder;
2727
import org.sliceworkz.eventmodeling.slices.FeatureSlice;
2828
import org.sliceworkz.eventmodeling.slices.FeatureSlice.Type;
29+
import org.sliceworkz.eventstore.infra.postgres.DatabaseInitMode;
2930

3031
@FeatureSlice(type = Type.AUTOMATION)
3132
public class CreateShippingLabelFeatureSlice implements OrderProcessingFeatureSlice{
3233

33-
private RequiredShippingLabels requiredShippingLabels;
34-
35-
@Override
36-
public void preConfigure(DataSource dataSource, boolean initializeDatabase) {
37-
requiredShippingLabels = new RequiredShippingLabels(dataSource);
38-
if ( initializeDatabase ) {
39-
this.requiredShippingLabels.initialize();
40-
}
41-
}
42-
43-
4434
@Override
4535
public void configureAutomation (
4636
BoundedContextBuilder<OrderProcessingDomainEvent, OrderProcessingInboundEvent, OrderProcessingOutboundEvent> builder) {
37+
var requiredShippingLabels = new RequiredShippingLabels(builder.port(DataSource.class));
38+
if ( builder.port(DatabaseInitMode.class) == DatabaseInitMode.INITIALIZE ) {
39+
requiredShippingLabels.initialize();
40+
}
4741
builder.readmodel(requiredShippingLabels);
4842
builder.automation(new CreateShippingLabelAutomation(requiredShippingLabels));
4943
}

sliceworkz-eventmodeling-benchmark/src/main/java/org/sliceworkz/eventmodeling/benchmark/features/dispatchorder/DispatchOrderFeatureSlice.java

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,23 +26,18 @@
2626
import org.sliceworkz.eventmodeling.boundedcontext.BoundedContextBuilder;
2727
import org.sliceworkz.eventmodeling.slices.FeatureSlice;
2828
import org.sliceworkz.eventmodeling.slices.FeatureSlice.Type;
29+
import org.sliceworkz.eventstore.infra.postgres.DatabaseInitMode;
2930

3031
@FeatureSlice(type = Type.AUTOMATION)
3132
public class DispatchOrderFeatureSlice implements OrderProcessingFeatureSlice {
32-
33-
private OrdersReadyToDispatch ordersReadyToDispatch;
34-
35-
@Override
36-
public void preConfigure(DataSource dataSource, boolean initializeDatabase) {
37-
ordersReadyToDispatch = new OrdersReadyToDispatch(dataSource);
38-
if (initializeDatabase) {
39-
this.ordersReadyToDispatch.initialize();
40-
}
41-
}
4233

4334
@Override
4435
public void configureAutomation(
4536
BoundedContextBuilder<OrderProcessingDomainEvent, OrderProcessingInboundEvent, OrderProcessingOutboundEvent> builder) {
37+
var ordersReadyToDispatch = new OrdersReadyToDispatch(builder.port(DataSource.class));
38+
if ( builder.port(DatabaseInitMode.class) == DatabaseInitMode.INITIALIZE ) {
39+
ordersReadyToDispatch.initialize();
40+
}
4641
builder.readmodel(ordersReadyToDispatch);
4742
builder.automation(new DispatchOrderAutomation(ordersReadyToDispatch));
4843
}

sliceworkz-eventmodeling-benchmark/src/main/java/org/sliceworkz/eventmodeling/benchmark/features/inboundorder/InboundOrderFeatureSlice.java

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717
*/
1818
package org.sliceworkz.eventmodeling.benchmark.features.inboundorder;
1919

20-
import javax.sql.DataSource;
21-
2220
import org.sliceworkz.eventmodeling.benchmark.OrderProcessingEvent.OrderProcessingDomainEvent;
2321
import org.sliceworkz.eventmodeling.benchmark.OrderProcessingEvent.OrderProcessingInboundEvent;
2422
import org.sliceworkz.eventmodeling.benchmark.OrderProcessingEvent.OrderProcessingOutboundEvent;
@@ -30,11 +28,6 @@
3028
@FeatureSlice(type = Type.TRANSLATION)
3129
public class InboundOrderFeatureSlice implements OrderProcessingFeatureSlice {
3230

33-
@Override
34-
public void preConfigure(DataSource dataSource, boolean initializeDatabase) {
35-
}
36-
37-
3831
@Override
3932
public void configureAutomation(
4033
BoundedContextBuilder<OrderProcessingDomainEvent, OrderProcessingInboundEvent, OrderProcessingOutboundEvent> builder) {

sliceworkz-eventmodeling-benchmark/src/main/java/org/sliceworkz/eventmodeling/benchmark/features/packageorder/PackageOrderFeatureSlice.java

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,23 +26,18 @@
2626
import org.sliceworkz.eventmodeling.boundedcontext.BoundedContextBuilder;
2727
import org.sliceworkz.eventmodeling.slices.FeatureSlice;
2828
import org.sliceworkz.eventmodeling.slices.FeatureSlice.Type;
29+
import org.sliceworkz.eventstore.infra.postgres.DatabaseInitMode;
2930

3031
@FeatureSlice(type=Type.AUTOMATION)
3132
public class PackageOrderFeatureSlice implements OrderProcessingFeatureSlice{
3233

33-
private OrdersReadyToPackage ordersReadyToPackage;
34-
35-
@Override
36-
public void preConfigure(DataSource dataSource, boolean initializeDatabase) {
37-
ordersReadyToPackage = new OrdersReadyToPackage(dataSource);
38-
if ( initializeDatabase ) {
39-
this.ordersReadyToPackage.initialize();
40-
}
41-
}
42-
4334
@Override
4435
public void configureAutomation(
4536
BoundedContextBuilder<OrderProcessingDomainEvent, OrderProcessingInboundEvent, OrderProcessingOutboundEvent> builder) {
37+
var ordersReadyToPackage = new OrdersReadyToPackage(builder.port(DataSource.class));
38+
if ( builder.port(DatabaseInitMode.class) == DatabaseInitMode.INITIALIZE ) {
39+
ordersReadyToPackage.initialize();
40+
}
4641
builder.readmodel(ordersReadyToPackage);
4742
builder.automation(new PackageOrderAutomation(ordersReadyToPackage));
4843
}

0 commit comments

Comments
 (0)