Skip to content

Commit 1a6f902

Browse files
committed
Added sample of stream transformations and README for events versioning samples
1 parent 3e01cb7 commit 1a6f902

8 files changed

Lines changed: 878 additions & 86 deletions

File tree

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Tutorial, practical samples and other resources about Event Sourcing in JVM. See
2525
- [Main assumptions](#main-assumptions)
2626
- [Prerequisites](#prerequisites)
2727
- [Tools used](#tools-used)
28+
- [Event Versioning](#event-versioning)
2829
- [Articles](#articles)
2930

3031
## Event Sourcing
@@ -267,6 +268,21 @@ For running the Event Store examples you need to have:
267268
2. [PostgreSQL](https://www.postgresql.org/) - Read Models
268269
3. [Spring Boot](https://spring.io/projects/spring-boot) - Web Application framework
269270

271+
### [Event Versioning](./samples/events-versioning)
272+
Shows how to handle basic event schema versioning scenarios using event and stream transformations (e.g. upcasting):
273+
- [Simple mapping](./samples/events-versioning/#simple-mapping)
274+
- [New not required property](./samples/events-versioning/#new-not-required-property)
275+
- [New required property](./samples/events-versioning/#new-required-property)
276+
- [Renamed property](./samples/events-versioning/#renamed-property)
277+
- [Upcasting](./samples/events-versioning/#upcasting)
278+
- [Changed Structure](./samples/events-versioning/#changed-structure)
279+
- [New required property](./samples/events-versioning/#new-required-property-1)
280+
- [Downcasters](./samples/events-versioning/#downcasters)
281+
- [Events Transformations](./samples/events-versioning/#events-transformations)
282+
- [Stream Transformation](./samples/events-versioning/#stream-transformation)
283+
- [Summary](./samples/events-versioning/#summary)
284+
- 📝 [Simple patterns for events schema versioning](https://event-driven.io/en/simple_events_versioning_patterns/?utm_source=event_sourcing_jvm_)
285+
270286
## Articles
271287

272288
Read also more on the **Event Sourcing** and **CQRS** topics in my [blog](https://event-driven.io/?utm_source=event_sourcing_jvm) posts:

samples/events-versioning/README.md

Lines changed: 561 additions & 82 deletions
Large diffs are not rendered by default.
685 KB
Loading
610 KB
Loading

samples/events-versioning/src/main/java/io/eventdriven/eventsversioning/v1/productitems/PricedProductItem.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
public record PricedProductItem(
44
ProductItem productItem,
55
double unitPrice
6-
) {};
6+
) {}

samples/events-versioning/src/test/java/io/eventdriven/eventsversioning/simplemappings/NewNotRequiredPropertyTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public record ShoppingCartOpened(
1515
UUID clientId,
1616
// Adding new not required property
1717
LocalDateTime initializedAt
18-
) { };
18+
) { }
1919

2020
@Test
2121
public void Should_BeForwardCompatible()

samples/events-versioning/src/test/java/io/eventdriven/eventsversioning/transformations/MultipleTransformationsWithDifferentEventTypesTests.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@
1212
import java.util.function.Function;
1313

1414
import static java.util.stream.Collectors.toMap;
15-
import static org.junit.jupiter.api.Assertions.assertEquals;
16-
import static org.junit.jupiter.api.Assertions.assertTrue;
15+
import static org.junit.jupiter.api.Assertions.*;
1716

1817
public class MultipleTransformationsWithDifferentEventTypesTests {
1918
public record Client(
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
package io.eventdriven.eventsversioning.transformations;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import io.eventdriven.eventsversioning.serialization.Serializer;
5+
import io.eventdriven.eventsversioning.v1.ShoppingCartEvent;
6+
import io.eventdriven.eventsversioning.v1.productitems.PricedProductItem;
7+
import io.eventdriven.eventsversioning.v1.productitems.ProductItem;
8+
import org.junit.jupiter.api.Test;
9+
10+
import java.time.LocalDateTime;
11+
import java.util.*;
12+
import java.util.function.Function;
13+
import java.util.stream.Stream;
14+
15+
import static org.junit.jupiter.api.Assertions.assertEquals;
16+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
17+
18+
public class StreamTransformationsTests {
19+
public record ShoppingCartInitializedWithProducts(
20+
UUID shoppingCartId,
21+
UUID clientId,
22+
List<PricedProductItem> productItems
23+
) {
24+
}
25+
26+
public record EventMetadata(
27+
UUID correlationId
28+
) {
29+
}
30+
31+
public record EventData(
32+
String eventType,
33+
byte[] data,
34+
byte[] metaData
35+
) {
36+
}
37+
38+
public static List<EventData> flattenInitializedEventsWithProductItemsAdded(List<EventData> events) {
39+
var cartOpened = events.get(0);
40+
var cartInitializedCorrelationId =
41+
Serializer.deserialize(EventMetadata.class, cartOpened.metaData)
42+
.orElseThrow(() -> new RuntimeException("Error deserializing metadata"))
43+
.correlationId;
44+
45+
var i = 1;
46+
var productItemsAdded = new ArrayList<EventData>();
47+
48+
while (i < events.size()) {
49+
var eventData = events.get(i);
50+
51+
if (!eventData.eventType().equals("product_item_added_v1"))
52+
break;
53+
54+
var correlationId =
55+
Serializer.deserialize(EventMetadata.class, eventData.metaData)
56+
.orElseThrow(() -> new RuntimeException("Error deserializing metadata"))
57+
.correlationId;
58+
59+
if (!correlationId.equals(cartInitializedCorrelationId))
60+
break;
61+
62+
productItemsAdded.add(eventData);
63+
i++;
64+
}
65+
66+
var mergedEvent = toShoppingCartInitializedWithProducts(
67+
cartOpened,
68+
productItemsAdded
69+
);
70+
71+
return Stream.concat(
72+
Stream.of(mergedEvent),
73+
events.stream().skip(i)
74+
).toList();
75+
}
76+
77+
private static EventData toShoppingCartInitializedWithProducts(
78+
EventData shoppingCartInitialized,
79+
List<EventData> productItemsAdded
80+
) {
81+
var shoppingCartInitializedJson =
82+
Serializer.deserialize(shoppingCartInitialized.data);
83+
84+
var newEvent = new ShoppingCartInitializedWithProducts(
85+
UUID.fromString(shoppingCartInitializedJson.at("/shoppingCartId").asText()),
86+
UUID.fromString(shoppingCartInitializedJson.at("/clientId").asText()),
87+
productItemsAdded.stream()
88+
.map(pi -> {
89+
var pricedProductItem = Serializer.deserialize(pi.data);
90+
91+
return new PricedProductItem(
92+
new ProductItem(
93+
UUID.fromString(pricedProductItem.at("/productItem/productItem/productId").asText()),
94+
pricedProductItem.at("/productItem/productItem/quantity").asInt()
95+
),
96+
pricedProductItem.at("/productItem/unitPrice").asDouble()
97+
);
98+
}).toList()
99+
);
100+
101+
return new EventData(
102+
"shopping_cart_initialized_v2",
103+
Serializer.serialize(newEvent),
104+
shoppingCartInitialized.metaData
105+
);
106+
}
107+
108+
public class StreamTransformations {
109+
private final List<Function<List<EventData>, List<EventData>>> jsonTransformations = new ArrayList<>();
110+
111+
public List<EventData> transform(List<EventData> events) {
112+
if (jsonTransformations.isEmpty())
113+
return events;
114+
115+
var result = events;
116+
117+
for (var transform : jsonTransformations) {
118+
result = transform.apply(result);
119+
}
120+
return result;
121+
}
122+
123+
public StreamTransformations register(Function<List<EventData>, List<EventData>> transformJson) {
124+
jsonTransformations.add(transformJson);
125+
return this;
126+
}
127+
}
128+
129+
public class EventTransformations {
130+
private final Map<String, Function<byte[], Object>> jsonTransformations = new HashMap<>();
131+
132+
public Optional<Object> tryTransform(String eventTypeName, byte[] json) {
133+
if (!jsonTransformations.containsKey(eventTypeName)) {
134+
return Optional.empty();
135+
}
136+
137+
var transformJson = jsonTransformations.get(eventTypeName);
138+
139+
return Optional.of(transformJson.apply(json));
140+
}
141+
142+
public <Event> EventTransformations register(
143+
String eventTypeName,
144+
Function<JsonNode, Event> transformJson
145+
) {
146+
jsonTransformations.put(
147+
eventTypeName,
148+
json -> transformJson.apply(Serializer.deserialize(json))
149+
);
150+
return this;
151+
}
152+
153+
public <OldEvent, Event> EventTransformations register(
154+
Class<OldEvent> oldEventClass,
155+
String eventTypeName,
156+
Function<OldEvent, Event> transformEvent
157+
) {
158+
jsonTransformations.put(
159+
eventTypeName,
160+
json -> transformEvent.apply(Serializer.deserialize(oldEventClass, json)
161+
.orElseThrow(() -> new RuntimeException("Error deserializing")))
162+
);
163+
return this;
164+
}
165+
}
166+
167+
public class EventTypeMapping {
168+
private final Map<String, Class> mappings = new HashMap<>();
169+
170+
public <Event> EventTypeMapping register(Class<Event> eventClass, String... typeNames) {
171+
for (var typeName : typeNames) {
172+
mappings.put(typeName, eventClass);
173+
}
174+
175+
return this;
176+
}
177+
178+
public Class map(String eventType) {
179+
return mappings.get(eventType);
180+
}
181+
}
182+
183+
public record EventSerializer(
184+
EventTypeMapping mapping,
185+
StreamTransformations streamTransformations,
186+
EventTransformations transformations
187+
) {
188+
public Optional<Object> deserialize(String eventTypeName, byte[] json) {
189+
return transformations.tryTransform(eventTypeName, json)
190+
.or(() -> Serializer.deserialize(mapping().map(eventTypeName), json));
191+
}
192+
193+
public List<Optional<Object>> deserialize(List<EventData> events) {
194+
return streamTransformations.transform(events).stream()
195+
.map(event -> deserialize(event.eventType, event.data))
196+
.toList();
197+
}
198+
}
199+
200+
public record EventEnvelope(
201+
String type,
202+
Object data,
203+
EventMetadata metadata
204+
) {
205+
}
206+
207+
@Test
208+
public void UpcastObjects_Should_BeForwardCompatible() {
209+
// Given
210+
var mapping = new EventTypeMapping()
211+
.register(ShoppingCartInitializedWithProducts.class, "shopping_cart_initialized_v2")
212+
.register(ShoppingCartEvent.ProductItemAddedToShoppingCart.class, "product_item_added_v1")
213+
.register(ShoppingCartEvent.ShoppingCartConfirmed.class, "shopping_card_confirmed_v1");
214+
215+
var streamTransformations =
216+
new StreamTransformations()
217+
.register(StreamTransformationsTests::flattenInitializedEventsWithProductItemsAdded);
218+
219+
var serializer = new EventSerializer(
220+
mapping,
221+
streamTransformations,
222+
new EventTransformations()
223+
);
224+
225+
var shoppingCardId = UUID.randomUUID();
226+
var clientId = UUID.randomUUID();
227+
var theSameCorrelationId = UUID.randomUUID();
228+
var productItem = new PricedProductItem(new ProductItem(UUID.randomUUID(), 1), 23.22);
229+
230+
var events = new ArrayList<>(List.of(
231+
new EventEnvelope(
232+
"shopping_cart_initialized_v1",
233+
new ShoppingCartEvent.ShoppingCartOpened(shoppingCardId, clientId),
234+
new EventMetadata(theSameCorrelationId)
235+
),
236+
new EventEnvelope(
237+
"product_item_added_v1",
238+
new ShoppingCartEvent.ProductItemAddedToShoppingCart(shoppingCardId, productItem),
239+
new EventMetadata(theSameCorrelationId)
240+
),
241+
new EventEnvelope(
242+
"product_item_added_v1",
243+
new ShoppingCartEvent.ProductItemAddedToShoppingCart(shoppingCardId, productItem),
244+
new EventMetadata(theSameCorrelationId)
245+
),
246+
new EventEnvelope(
247+
"product_item_added_v1",
248+
new ShoppingCartEvent.ProductItemAddedToShoppingCart(shoppingCardId, productItem),
249+
new EventMetadata(UUID.randomUUID())
250+
),
251+
new EventEnvelope(
252+
"shopping_card_confirmed_v1",
253+
new ShoppingCartEvent.ShoppingCartConfirmed(shoppingCardId, LocalDateTime.now()),
254+
new EventMetadata(UUID.randomUUID())
255+
)
256+
));
257+
258+
var serialisedEvents = events.stream()
259+
.map(e ->
260+
new EventData(
261+
e.type,
262+
Serializer.serialize(e.data()),
263+
Serializer.serialize(e.metadata)
264+
)
265+
).toList();
266+
267+
// When
268+
var deserializedEvents = serializer.deserialize(serialisedEvents).stream()
269+
.filter(Optional::isPresent)
270+
.map(Optional::get)
271+
.toList();
272+
273+
// Then
274+
assertEquals(3, deserializedEvents.size());
275+
276+
assertInstanceOf(ShoppingCartInitializedWithProducts.class, deserializedEvents.get(0));
277+
var shoppingCartInitializedWithProducts = (ShoppingCartInitializedWithProducts)deserializedEvents.get(0);
278+
279+
assertEquals(shoppingCardId, shoppingCartInitializedWithProducts.shoppingCartId());
280+
assertEquals(clientId, shoppingCartInitializedWithProducts.clientId());
281+
assertEquals(2, shoppingCartInitializedWithProducts.productItems().size());
282+
assertEquals(productItem, shoppingCartInitializedWithProducts.productItems().get(0));
283+
assertEquals(productItem, shoppingCartInitializedWithProducts.productItems().get(1));
284+
285+
286+
assertInstanceOf(ShoppingCartEvent.ProductItemAddedToShoppingCart.class, deserializedEvents.get(1));
287+
var productItemAddedToShoppingCart = (ShoppingCartEvent.ProductItemAddedToShoppingCart)deserializedEvents.get(1);
288+
289+
assertEquals(shoppingCardId, productItemAddedToShoppingCart.shoppingCartId());
290+
assertEquals(productItem, productItemAddedToShoppingCart.productItem());
291+
292+
293+
assertInstanceOf(ShoppingCartEvent.ShoppingCartConfirmed.class, deserializedEvents.get(2));
294+
var shoppingCartConfirmed = (ShoppingCartEvent.ShoppingCartConfirmed)deserializedEvents.get(2);
295+
296+
assertEquals(shoppingCardId, shoppingCartConfirmed.shoppingCartId());
297+
}
298+
}

0 commit comments

Comments
 (0)