Skip to content

Commit e7faafc

Browse files
committed
add aggregate support
1 parent d3b3d9e commit e7faafc

File tree

16 files changed

+684
-6
lines changed

16 files changed

+684
-6
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Sliceworkz Event Modeling - an opinionated Event Modeling framework in Java
3+
* Copyright © 2025 Sliceworkz / XTi ([email protected])
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.aggregates;
19+
20+
import org.sliceworkz.eventstore.events.EventHandler;
21+
22+
public interface Aggregate<DOMAIN_EVENT_TYPE> extends EventHandler<DOMAIN_EVENT_TYPE> {
23+
24+
void setContext ( AggregateContext<DOMAIN_EVENT_TYPE> aggregateContext );
25+
26+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Sliceworkz Event Modeling - an opinionated Event Modeling framework in Java
3+
* Copyright © 2025 Sliceworkz / XTi ([email protected])
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.aggregates;
19+
20+
import org.sliceworkz.eventstore.events.Tags;
21+
22+
public interface AggregateCapability<DOMAIN_EVENT_TYPE> {
23+
24+
<T extends Aggregate<DOMAIN_EVENT_TYPE>> T aggregate ( Class<T> aggregateClass, Tags identity );
25+
26+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Sliceworkz Event Modeling - an opinionated Event Modeling framework in Java
3+
* Copyright © 2025 Sliceworkz / XTi ([email protected])
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.aggregates;
19+
20+
import java.util.List;
21+
22+
import org.sliceworkz.eventstore.events.Tags;
23+
24+
public interface AggregateContext<DOMAIN_EVENT_TYPE> {
25+
26+
Tags identity ( );
27+
28+
void raiseEvent ( DOMAIN_EVENT_TYPE event );
29+
30+
void raiseEvent ( DOMAIN_EVENT_TYPE event, String idempotencyKey );
31+
32+
void raiseEvents ( List<DOMAIN_EVENT_TYPE> events );
33+
34+
AggregateEventAppender<DOMAIN_EVENT_TYPE> eventAppender ( );
35+
36+
void updateFromStream ( );
37+
38+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Sliceworkz Event Modeling - an opinionated Event Modeling framework in Java
3+
* Copyright © 2025 Sliceworkz / XTi ([email protected])
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.aggregates;
19+
20+
public interface AggregateEventAppender<DOMAIN_EVENT_TYPE> {
21+
22+
AggregateEventAppender<DOMAIN_EVENT_TYPE> add ( DOMAIN_EVENT_TYPE event );
23+
24+
AggregateEventAppender<DOMAIN_EVENT_TYPE> add ( DOMAIN_EVENT_TYPE event, String idempotencyKey );
25+
26+
void append ( );
27+
28+
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@
1717
*/
1818
package org.sliceworkz.eventmodeling.boundedcontext;
1919

20+
import org.sliceworkz.eventmodeling.aggregates.AggregateCapability;
21+
2022
public interface AllCapabilities<DOMAIN_EVENT_TYPE, INBOUND_EVENT_TYPE, OUTBOUND_EVENT_TYPE> extends
2123
CQRSCapabilities<DOMAIN_EVENT_TYPE>,
22-
DCBCapabilities<DOMAIN_EVENT_TYPE, INBOUND_EVENT_TYPE, OUTBOUND_EVENT_TYPE>,
24+
DCBCapabilities<DOMAIN_EVENT_TYPE, INBOUND_EVENT_TYPE, OUTBOUND_EVENT_TYPE>,
25+
AggregateCapability<DOMAIN_EVENT_TYPE>,
2326
FeatureSliceCapabilities<DOMAIN_EVENT_TYPE, INBOUND_EVENT_TYPE, OUTBOUND_EVENT_TYPE> {
2427

2528
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import java.util.function.Predicate;
2121

22+
import org.sliceworkz.eventmodeling.aggregates.Aggregate;
2223
import org.sliceworkz.eventmodeling.automation.Automation;
2324
import org.sliceworkz.eventmodeling.events.Instance;
2425
import org.sliceworkz.eventmodeling.inbound.Translator;
@@ -50,6 +51,8 @@ BoundedContextBuilder<DOMAIN_EVENT_TYPE, INBOUND_EVENT_TYPE, OUTBOUND_EVENT_TYPE
5051

5152
BoundedContextBuilder<DOMAIN_EVENT_TYPE, INBOUND_EVENT_TYPE, OUTBOUND_EVENT_TYPE> rootPackage(Package rootPackage);
5253

54+
BoundedContextBuilder<DOMAIN_EVENT_TYPE, INBOUND_EVENT_TYPE, OUTBOUND_EVENT_TYPE> aggregate(Class<? extends Aggregate<DOMAIN_EVENT_TYPE>> aggregateClass);
55+
5356
LiveModelSpecification<DOMAIN_EVENT_TYPE, INBOUND_EVENT_TYPE, OUTBOUND_EVENT_TYPE> readmodel(Class<? extends ReadModelWithMetaData<DOMAIN_EVENT_TYPE>> readModelClass);
5457

5558
LongLivedReadModelSpecification<DOMAIN_EVENT_TYPE, INBOUND_EVENT_TYPE, OUTBOUND_EVENT_TYPE> readmodel(ReadModelWithMetaData<DOMAIN_EVENT_TYPE> readModel);
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Sliceworkz Event Modeling - an opinionated Event Modeling framework in Java
3+
* Copyright © 2025 Sliceworkz / XTi ([email protected])
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.module.aggregates;
19+
20+
import java.util.List;
21+
22+
import org.sliceworkz.eventmodeling.aggregates.Aggregate;
23+
import org.sliceworkz.eventmodeling.aggregates.AggregateContext;
24+
import org.sliceworkz.eventmodeling.aggregates.AggregateEventAppender;
25+
import org.sliceworkz.eventstore.events.EventReference;
26+
import org.sliceworkz.eventstore.events.Tags;
27+
import org.sliceworkz.eventstore.projection.Projection;
28+
import org.sliceworkz.eventstore.projection.Projector;
29+
import org.sliceworkz.eventstore.stream.EventStream;
30+
31+
public class AggregateContextImpl<DOMAIN_EVENT_TYPE> implements AggregateContext<DOMAIN_EVENT_TYPE> {
32+
33+
private Tags identity;
34+
private Aggregate<DOMAIN_EVENT_TYPE> aggregate;
35+
private EventStream<DOMAIN_EVENT_TYPE> eventStream;
36+
private Projection<DOMAIN_EVENT_TYPE> projectionTowardsAggregate;
37+
private EventReference lastEventReference;
38+
private AggregateEventAppender<DOMAIN_EVENT_TYPE> aggregateEventAppender;
39+
40+
public AggregateContextImpl ( Tags identity, Aggregate<DOMAIN_EVENT_TYPE> aggregate, EventStream<DOMAIN_EVENT_TYPE> eventStream ) {
41+
this.identity = identity;
42+
this.aggregate = aggregate;
43+
this.eventStream = eventStream;
44+
this.projectionTowardsAggregate = new ProjectionTowardsAggregate<>(aggregate, identity);
45+
this.aggregateEventAppender = new AggregateEventAppenderImpl<>(eventStream, aggregate, identity, null);
46+
}
47+
48+
@Override
49+
public Tags identity() {
50+
return identity;
51+
}
52+
53+
@Override
54+
public void raiseEvent(DOMAIN_EVENT_TYPE event) {
55+
raiseEvent(event, null);
56+
}
57+
58+
@Override
59+
public void raiseEvent(DOMAIN_EVENT_TYPE event, String idempotencyKey) {
60+
eventAppender().add(event, idempotencyKey).append();
61+
}
62+
63+
@Override
64+
public void raiseEvents(List<DOMAIN_EVENT_TYPE> events) {
65+
var eventAppender = eventAppender();
66+
events.stream().map(eventAppender::add);
67+
eventAppender.append();
68+
}
69+
70+
@Override
71+
public AggregateEventAppender<DOMAIN_EVENT_TYPE> eventAppender() {
72+
return aggregateEventAppender;
73+
}
74+
75+
@Override
76+
public void updateFromStream() {
77+
this.lastEventReference = Projector.from(eventStream).towards(projectionTowardsAggregate).startingAfter(lastEventReference).build().run().lastEventReference();
78+
this.aggregateEventAppender = new AggregateEventAppenderImpl<>(eventStream, aggregate, identity, lastEventReference);
79+
}
80+
81+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Sliceworkz Event Modeling - an opinionated Event Modeling framework in Java
3+
* Copyright © 2025 Sliceworkz / XTi ([email protected])
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.module.aggregates;
19+
20+
import java.util.ArrayList;
21+
import java.util.List;
22+
import java.util.Optional;
23+
24+
import org.sliceworkz.eventmodeling.aggregates.Aggregate;
25+
import org.sliceworkz.eventmodeling.aggregates.AggregateEventAppender;
26+
import org.sliceworkz.eventstore.events.EphemeralEvent;
27+
import org.sliceworkz.eventstore.events.Event;
28+
import org.sliceworkz.eventstore.events.EventReference;
29+
import org.sliceworkz.eventstore.events.Tags;
30+
import org.sliceworkz.eventstore.query.EventQuery;
31+
import org.sliceworkz.eventstore.query.EventTypesFilter;
32+
import org.sliceworkz.eventstore.stream.AppendCriteria;
33+
import org.sliceworkz.eventstore.stream.EventStream;
34+
35+
public class AggregateEventAppenderImpl<DOMAIN_EVENT_TYPE> implements AggregateEventAppender<DOMAIN_EVENT_TYPE> {
36+
37+
private List<EphemeralEvent<? extends DOMAIN_EVENT_TYPE>> events = new ArrayList<>();
38+
private Aggregate<DOMAIN_EVENT_TYPE> aggregate;
39+
private EventStream<DOMAIN_EVENT_TYPE> eventStream;
40+
private Tags identity;
41+
private EventReference lastReference;
42+
43+
public AggregateEventAppenderImpl ( EventStream<DOMAIN_EVENT_TYPE> eventStream, Aggregate<DOMAIN_EVENT_TYPE> aggregate, Tags identity, EventReference lastReference ) {
44+
this.eventStream = eventStream;
45+
this.aggregate = aggregate;
46+
this.identity = identity;
47+
this.lastReference = lastReference;
48+
}
49+
50+
@Override
51+
public AggregateEventAppenderImpl<DOMAIN_EVENT_TYPE> add(DOMAIN_EVENT_TYPE event) {
52+
return add(event, null);
53+
}
54+
55+
@Override
56+
public AggregateEventAppenderImpl<DOMAIN_EVENT_TYPE> add(DOMAIN_EVENT_TYPE event, String idempotencyKey) {
57+
events.add(Event.of(event, identity).withIdempotencyKey(idempotencyKey));
58+
return this;
59+
}
60+
61+
@Override
62+
public void append() {
63+
eventStream.append(
64+
AppendCriteria.of(EventQuery.forEvents(EventTypesFilter.any(), identity), Optional.ofNullable(lastReference)),
65+
events).stream().map(e->{this.lastReference=e.reference();return e;}).forEach(aggregate::when);
66+
events.clear();
67+
}
68+
69+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Sliceworkz Event Modeling - an opinionated Event Modeling framework in Java
3+
* Copyright © 2025 Sliceworkz / XTi ([email protected])
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.module.aggregates;
19+
20+
import java.lang.reflect.Constructor;
21+
import java.lang.reflect.InvocationTargetException;
22+
import java.util.Collection;
23+
import java.util.HashMap;
24+
import java.util.Map;
25+
26+
import org.slf4j.Logger;
27+
import org.slf4j.LoggerFactory;
28+
import org.sliceworkz.eventmodeling.aggregates.Aggregate;
29+
import org.sliceworkz.eventmodeling.aggregates.AggregateCapability;
30+
import org.sliceworkz.eventstore.events.Tags;
31+
import org.sliceworkz.eventstore.stream.EventStream;
32+
33+
// TODO add micrometer for observability
34+
public class AggregateModule<DOMAIN_EVENT_TYPE> implements AggregateCapability<DOMAIN_EVENT_TYPE> {
35+
36+
private static final Logger LOGGER = LoggerFactory.getLogger(AggregateModule.class);
37+
38+
private Map<Class<? extends Aggregate<DOMAIN_EVENT_TYPE>>,Constructor<? extends Aggregate<DOMAIN_EVENT_TYPE>>> aggregateClassesWithConstructor = new HashMap<>();
39+
private EventStream<DOMAIN_EVENT_TYPE> domainEventStream;
40+
private String boundedContext;
41+
42+
public AggregateModule ( String boundedContext, Collection<Class<? extends Aggregate<DOMAIN_EVENT_TYPE>>> aggregateClasses, EventStream<DOMAIN_EVENT_TYPE> domainEventStream ) {
43+
this.boundedContext = boundedContext;
44+
this.domainEventStream = domainEventStream;
45+
aggregateClasses.forEach(aggregateClass->{
46+
try {
47+
aggregateClassesWithConstructor.put(aggregateClass, aggregateClass.getDeclaredConstructor(new Class[] {}));
48+
} catch (NoSuchMethodException | SecurityException e) {
49+
LOGGER.error(e.getMessage(), e);
50+
throw new RuntimeException(e);
51+
}
52+
});
53+
54+
LOGGER.info("aggregates: %s".formatted(aggregateClasses));
55+
}
56+
57+
@SuppressWarnings("unchecked")
58+
@Override
59+
public <T extends Aggregate<DOMAIN_EVENT_TYPE>> T aggregate(Class<T> aggregateClass, Tags identity) {
60+
61+
if ( aggregateClassesWithConstructor.containsKey(aggregateClass)) {
62+
63+
if ( identity == null || identity.tags().size() < 1 ) {
64+
throw new IllegalArgumentException("aggregate identity needs at least one tag, got '%s'".formatted(identity));
65+
}
66+
67+
T result;
68+
try {
69+
result = (T) aggregateClassesWithConstructor.get(aggregateClass).newInstance(new Object[] {});
70+
71+
AggregateContextImpl<DOMAIN_EVENT_TYPE> aci = new AggregateContextImpl<DOMAIN_EVENT_TYPE> (identity, result, domainEventStream);
72+
result.setContext(aci);
73+
aci.updateFromStream();
74+
75+
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException
76+
| InvocationTargetException | SecurityException e) {
77+
LOGGER.error(e.getMessage(), e);
78+
throw new RuntimeException(e);
79+
}
80+
return result;
81+
} else {
82+
throw new IllegalArgumentException("aggregate class '%s' not registered in bounded context '%s'".formatted(aggregateClass, boundedContext));
83+
}
84+
}
85+
86+
}

0 commit comments

Comments
 (0)