From d9fa06390897d22c572fc8355944072f2ae499b5 Mon Sep 17 00:00:00 2001 From: Mateusz Nowak Date: Tue, 8 Jul 2025 00:28:53 +0200 Subject: [PATCH 01/11] =?UTF-8?q?=F0=9F=93=B8=20chore:=20Creature=20Recrui?= =?UTF-8?q?tment=20|=20Add=20snapshotting=20to=20Dwelling=20aggregate=20wi?= =?UTF-8?q?th=205-event=20threshold?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Dwelling aggregate is now configured to use the existing dwellingSnapshotTrigger bean to create snapshots every 5 events. It's just to show how the snapshotting can be configured. --- .../CreatureRecruitmentConfiguration.java | 8 ++++++++ .../heroesofddd/creaturerecruitment/write/Dwelling.java | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/CreatureRecruitmentConfiguration.java b/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/CreatureRecruitmentConfiguration.java index 99753bf..88b0299 100644 --- a/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/CreatureRecruitmentConfiguration.java +++ b/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/CreatureRecruitmentConfiguration.java @@ -3,6 +3,9 @@ import com.dddheroes.heroesofddd.creaturerecruitment.write.recruitcreature.RecruitCreature; import com.dddheroes.heroesofddd.resourcespool.application.CommandCostResolver; import com.dddheroes.heroesofddd.shared.domain.valueobjects.Resources; +import org.axonframework.eventsourcing.EventCountSnapshotTriggerDefinition; +import org.axonframework.eventsourcing.SnapshotTriggerDefinition; +import org.axonframework.eventsourcing.Snapshotter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -23,4 +26,9 @@ public Class supportedCommandType() { } }; } + + @Bean + SnapshotTriggerDefinition dwellingSnapshotTrigger(Snapshotter snapshotter) { + return new EventCountSnapshotTriggerDefinition(snapshotter, 5); + } } diff --git a/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/write/Dwelling.java b/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/write/Dwelling.java index 5cf67ad..08e5d15 100644 --- a/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/write/Dwelling.java +++ b/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/write/Dwelling.java @@ -22,7 +22,7 @@ import static org.axonframework.modelling.command.AggregateLifecycle.*; -@Aggregate +@Aggregate(snapshotTriggerDefinition = "dwellingSnapshotTrigger") public class Dwelling { @AggregateIdentifier From 599ec75415fbb7658cb2586ad48dd1e10f0c0de7 Mon Sep 17 00:00:00 2001 From: Mateusz Nowak Date: Tue, 8 Jul 2025 11:51:04 +0200 Subject: [PATCH 02/11] =?UTF-8?q?=F0=9F=93=B8=20chore:=20Creature=20Recrui?= =?UTF-8?q?tment=20|=20snapshots=20-=20dwelling=20data=20needs=20to=20be?= =?UTF-8?q?=20public?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- generated-requests.http | 6 ++-- .../heroesofddd/GameConfiguration.java | 20 +++++++++++ .../creaturerecruitment/write/Dwelling.java | 8 ++--- .../DwellingIdSerializationModule.java | 35 +++++++++++++++++++ src/main/resources/application.yaml | 2 -- 5 files changed, 62 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/dddheroes/heroesofddd/shared/infrastructure/serialization/DwellingIdSerializationModule.java diff --git a/generated-requests.http b/generated-requests.http index 2c3fc32..9b5bf83 100644 --- a/generated-requests.http +++ b/generated-requests.http @@ -1,7 +1,7 @@ ### BuildDwelling -@gameId = scenario-1 -@playerId = player-1 -@dwellingId = dwelling-1 +@gameId = scenario-3 +@playerId = player-3 +@dwellingId = dwelling-3 ### BuildDwelling PUT http://localhost:8080/games/{{gameId}}/dwellings/{{dwellingId}} diff --git a/src/main/java/com/dddheroes/heroesofddd/GameConfiguration.java b/src/main/java/com/dddheroes/heroesofddd/GameConfiguration.java index 383bc64..efd1688 100644 --- a/src/main/java/com/dddheroes/heroesofddd/GameConfiguration.java +++ b/src/main/java/com/dddheroes/heroesofddd/GameConfiguration.java @@ -1,13 +1,19 @@ package com.dddheroes.heroesofddd; import com.dddheroes.heroesofddd.shared.application.GameMetaData; +import com.dddheroes.heroesofddd.shared.infrastructure.serialization.DwellingIdSerializationModule; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.ObjectMapper; import org.axonframework.eventhandling.EventMessage; import org.axonframework.eventhandling.async.SequencingPolicy; import org.axonframework.messaging.correlation.CorrelationDataProvider; import org.axonframework.messaging.correlation.MessageOriginProvider; import org.axonframework.messaging.correlation.SimpleCorrelationDataProvider; +import org.axonframework.serialization.Serializer; +import org.axonframework.serialization.json.JacksonSerializer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; @Configuration public class GameConfiguration { @@ -26,4 +32,18 @@ public CorrelationDataProvider gameDataProvider() { public CorrelationDataProvider messageOriginProvider() { return new MessageOriginProvider(); } + + @Bean + public Module dwellingIdSerializationModule() { + return new DwellingIdSerializationModule(); + } + + @Bean + @Primary + public Serializer defaultSerializer(ObjectMapper objectMapper) { + objectMapper.registerModule(dwellingIdSerializationModule()); + return JacksonSerializer.builder() + .objectMapper(objectMapper) + .build(); + } } diff --git a/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/write/Dwelling.java b/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/write/Dwelling.java index 08e5d15..c440120 100644 --- a/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/write/Dwelling.java +++ b/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/write/Dwelling.java @@ -26,10 +26,10 @@ public class Dwelling { @AggregateIdentifier - private DwellingId dwellingId; - private CreatureId creatureId; - private Resources costPerTroop; - private Amount availableCreatures; + public DwellingId dwellingId; + public CreatureId creatureId; + public Resources costPerTroop; + public Amount availableCreatures; @CommandHandler @CreationPolicy(AggregateCreationPolicy.CREATE_IF_MISSING) // performance downside in comparison to constructor diff --git a/src/main/java/com/dddheroes/heroesofddd/shared/infrastructure/serialization/DwellingIdSerializationModule.java b/src/main/java/com/dddheroes/heroesofddd/shared/infrastructure/serialization/DwellingIdSerializationModule.java new file mode 100644 index 0000000..43f865a --- /dev/null +++ b/src/main/java/com/dddheroes/heroesofddd/shared/infrastructure/serialization/DwellingIdSerializationModule.java @@ -0,0 +1,35 @@ +package com.dddheroes.heroesofddd.shared.infrastructure.serialization; + +import com.dddheroes.heroesofddd.creaturerecruitment.write.DwellingId; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; + +import java.io.IOException; + +public class DwellingIdSerializationModule extends SimpleModule { + + public DwellingIdSerializationModule() { + addSerializer(DwellingId.class, new DwellingIdSerializer()); + addDeserializer(DwellingId.class, new DwellingIdDeserializer()); + } + + private static class DwellingIdSerializer extends JsonSerializer { + @Override + public void serialize(DwellingId value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeString(value.raw()); + } + } + + private static class DwellingIdDeserializer extends JsonDeserializer { + @Override + public DwellingId deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + String value = p.getValueAsString(); + return DwellingId.of(value); + } + } +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index b767eae..e7c9f30 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -17,8 +17,6 @@ spring: hibernate: dialect: org.hibernate.dialect.PostgreSQLDialect axon: - serializer: - general: jackson axonserver: enabled: true eventhandling: From 3d52dbf23c4adaf1cb45c4c4f8856bddee5bd1da Mon Sep 17 00:00:00 2001 From: Mateusz Nowak Date: Tue, 8 Jul 2025 12:00:40 +0200 Subject: [PATCH 03/11] add jpa event store config --- .cursorrules | 261 ++++++++++++++++++ generated-requests.http | 2 +- .../resources/application-jpa-eventstore.yaml | 11 + 3 files changed, 273 insertions(+), 1 deletion(-) create mode 100644 .cursorrules create mode 100644 src/main/resources/application-jpa-eventstore.yaml diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 0000000..7c03ff3 --- /dev/null +++ b/.cursorrules @@ -0,0 +1,261 @@ +# TypeScript Event Sourcing with Emmett - Cursor Rules + +You are an expert TypeScript developer implementing Event Sourcing using the Emmett library. Follow these patterns and principles based on the reference Java/Axon implementation at `/Users/mateusznowak/GitRepos/MateuszNaKodach/HeroesOfDomainDrivenDesign.EventSourcing.Java.Axon.Spring`. + +## Architecture Patterns + +### Vertical Slice Architecture +- **Reference**: Java project modules (armies, astrologers, calendar, creaturerecruitment, resourcespool) +- Organize code by business capabilities, not technical layers +- Each slice should be self-contained with its own: + - Commands and command handlers + - Events and event handlers + - Aggregates + - Read models and projectors + - Domain rules + - REST APIs + - Configuration + +**TypeScript Structure:** +``` +src/ + slices/ + creature-recruitment/ + write/ + commands/ + aggregates/ + domain-rules/ + read/ + projectors/ + queries/ + events/ + api/ + armies/ + write/ read/ events/ api/ + calendar/ + write/ read/ events/ api/ +``` + +### Event Sourcing with Emmett + +#### Aggregates +- **Reference**: `Dwelling.java` aggregate pattern +- Use Emmett's `aggregate` function to define aggregates +- Implement command handlers that validate business rules and emit events +- Implement event handlers that evolve aggregate state +- Keep aggregates focused on a single business concept + +```typescript +// Example based on Dwelling.java +export const dwelling = aggregate( + 'Dwelling', + { + // Command handlers (equivalent to @CommandHandler methods) + buildDwelling: (command: BuildDwelling, state: DwellingState | null) => { + // Domain rule validation (like OnlyNotBuiltBuildingCanBeBuild) + if (state !== null) { + throw new DomainRuleViolation('Only not built building can be built'); + } + + return [DwellingBuilt.create(command)]; + }, + + recruitCreature: (command: RecruitCreature, state: DwellingState) => { + // Multiple domain rules validation + validateRecruitmentRules(command, state); + + return [CreatureRecruited.create(command, state)]; + } + }, + + // Event handlers (equivalent to @EventSourcingHandler methods) + { + DwellingBuilt: (state: DwellingState | null, event: DwellingBuilt) => ({ + dwellingId: event.dwellingId, + creatureId: event.creatureId, + costPerTroop: event.costPerTroop, + availableCreatures: 0, + }), + + CreatureRecruited: (state: DwellingState, event: CreatureRecruited) => ({ + ...state, + availableCreatures: state.availableCreatures - event.quantity, + }), + } +); +``` + +#### Events +- **Reference**: Event structure from `CreatureRecruited.java` and sealed interface `DwellingEvent.java` +- Use TypeScript discriminated unions for event types +- Include static factory methods for event creation +- Implement proper serialization for persistence + +```typescript +// Based on DwellingEvent.java sealed interface pattern +export type DwellingEvent = + | DwellingBuilt + | AvailableCreaturesChanged + | CreatureRecruited; + +export interface CreatureRecruited { + type: 'CreatureRecruited'; + dwellingId: string; + creatureId: string; + toArmy: string; + quantity: number; + totalCost: Resources; + + // Static factory method like in Java + static create(command: RecruitCreature, state: DwellingState): CreatureRecruited; +} +``` + +#### Domain Rules +- **Reference**: `DomainRule.java` interface and implementations like `OnlyNotBuiltBuildingCanBeBuild.java` +- Implement business rules as separate, testable classes +- Use descriptive names that express business intent +- Include proper error messages + +```typescript +// Based on DomainRule.java pattern +export interface DomainRule { + isViolated(): boolean; + message(): string; + verify(): void; // throws if violated +} + +export class RecruitCreaturesNotExceedAvailable implements DomainRule { + constructor( + private readonly available: number, + private readonly requested: number + ) {} + + isViolated(): boolean { + return this.requested > this.available; + } + + message(): string { + return `Cannot recruit ${this.requested} creatures, only ${this.available} available`; + } + + verify(): void { + if (this.isViolated()) { + throw new DomainRuleViolation(this.message()); + } + } +} +``` + +### CQRS Implementation + +#### Commands +- **Reference**: Command pattern from Java (e.g., `BuildDwelling.java`) +- Implement commands as immutable data structures +- Include validation and factory methods +- Group related commands in the same slice + +#### Read Models & Projectors +- **Reference**: `DwellingReadModelProjector.java` pattern +- Use Emmett's projection capabilities +- Create separate optimized read models +- Handle event ordering and replay scenarios + +```typescript +// Based on DwellingReadModelProjector.java +export const dwellingReadModelProjector = projector({ + name: 'DwellingReadModel', + + eventHandlers: { + DwellingBuilt: async (event: DwellingBuilt, { store }) => { + await store.upsert('dwellings', { + id: event.dwellingId, + creatureId: event.creatureId, + costPerTroop: event.costPerTroop, + availableCreatures: 0, + }); + }, + + CreatureRecruited: async (event: CreatureRecruited, { store }) => { + await store.update('dwellings', event.dwellingId, dwelling => ({ + ...dwelling, + availableCreatures: dwelling.availableCreatures - event.quantity, + })); + }, + } +}); +``` + +### Process Managers (Sagas) +- **Reference**: `WhenCreatureRecruitedThenAddToArmyProcessor.java` +- Handle cross-aggregate coordination +- Implement compensation logic for failed operations +- Use descriptive naming that expresses the business process + +```typescript +// Based on WhenCreatureRecruitedThenAddToArmyProcessor.java +export const creatureRecruitmentSaga = saga({ + name: 'CreatureRecruitmentAutomation', + + eventHandlers: { + CreatureRecruited: async (event: CreatureRecruited, { commandBus }) => { + try { + await commandBus.send(AddCreatureToArmy.create( + event.toArmy, + event.creatureId, + event.quantity + )); + } catch (error) { + // Compensation action + await commandBus.send(IncreaseAvailableCreatures.create( + event.dwellingId, + event.creatureId, + event.quantity + )); + } + } + } +}); +``` + +## Code Organization Principles + +### File Structure +Follow the Java reference structure but adapted for TypeScript: +- Group by business capability (vertical slices) +- Separate write/read sides clearly +- Keep domain rules in dedicated files +- Co-locate related components + +### Naming Conventions +- **Reference**: Consistent naming from Java codebase +- Use descriptive, business-focused names +- Commands: imperative (BuildDwelling, RecruitCreature) +- Events: past tense (DwellingBuilt, CreatureRecruited) +- Domain Rules: express business constraints clearly + +### Testing Strategy +- **Reference**: Test structure from Java codebase +- Test aggregates in isolation +- Test domain rules separately +- Test projectors with event sequences +- Use descriptive test names that express business scenarios + +## Best Practices + +1. **Event Store as Source of Truth**: Like in the Java reference, treat events as the authoritative data source +2. **Aggregate Boundaries**: Keep aggregates small and focused, like Dwelling aggregate +3. **Cross-Aggregate Communication**: Use events and process managers, never direct calls +4. **Domain Language**: Use ubiquitous language consistently (creatures, dwellings, armies) +5. **Immutability**: Ensure events and commands are immutable +6. **Error Handling**: Implement proper domain exceptions and compensation actions + +## Configuration and Setup + +Set up Emmett with similar patterns to the Java Spring configuration: +- Configure event store connection +- Set up projection processing groups +- Implement command cost resolution (like `CommandCostResolver`) +- Configure cross-cutting concerns (metadata, tracing) + +Remember: This reference Java/Axon implementation shows mature Event Sourcing patterns. Adapt these concepts to Emmett's API while maintaining the same architectural principles and business logic organization. \ No newline at end of file diff --git a/generated-requests.http b/generated-requests.http index 9b5bf83..bd6c1db 100644 --- a/generated-requests.http +++ b/generated-requests.http @@ -1,7 +1,7 @@ ### BuildDwelling @gameId = scenario-3 @playerId = player-3 -@dwellingId = dwelling-3 +@dwellingId = dwelling-4 ### BuildDwelling PUT http://localhost:8080/games/{{gameId}}/dwellings/{{dwellingId}} diff --git a/src/main/resources/application-jpa-eventstore.yaml b/src/main/resources/application-jpa-eventstore.yaml new file mode 100644 index 0000000..6eb6dab --- /dev/null +++ b/src/main/resources/application-jpa-eventstore.yaml @@ -0,0 +1,11 @@ +axon: + axonserver: + enabled: false +# eventhandling: +# processors: + # (Optional) You can override processor settings here if needed +# eventstore: + # No need to specify storage-engine if axon-server is disabled and JPA is on classpath + # Axon will auto-configure JpaEventStorageEngine + +# Inherit datasource and JPA config from application.yaml \ No newline at end of file From 995e4c4162bf599c997022523ccbdf2e57393e04 Mon Sep 17 00:00:00 2001 From: Mateusz Nowak Date: Tue, 8 Jul 2025 12:11:15 +0200 Subject: [PATCH 04/11] add jpa event store config --- generated-requests.http | 2 +- .../dddheroes/heroesofddd/GameConfiguration.java | 16 ++++++++-------- src/main/resources/application.yaml | 4 ++++ 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/generated-requests.http b/generated-requests.http index bd6c1db..9627b58 100644 --- a/generated-requests.http +++ b/generated-requests.http @@ -1,7 +1,7 @@ ### BuildDwelling @gameId = scenario-3 @playerId = player-3 -@dwellingId = dwelling-4 +@dwellingId = dwelling-5 ### BuildDwelling PUT http://localhost:8080/games/{{gameId}}/dwellings/{{dwellingId}} diff --git a/src/main/java/com/dddheroes/heroesofddd/GameConfiguration.java b/src/main/java/com/dddheroes/heroesofddd/GameConfiguration.java index efd1688..b5d3264 100644 --- a/src/main/java/com/dddheroes/heroesofddd/GameConfiguration.java +++ b/src/main/java/com/dddheroes/heroesofddd/GameConfiguration.java @@ -38,12 +38,12 @@ public Module dwellingIdSerializationModule() { return new DwellingIdSerializationModule(); } - @Bean - @Primary - public Serializer defaultSerializer(ObjectMapper objectMapper) { - objectMapper.registerModule(dwellingIdSerializationModule()); - return JacksonSerializer.builder() - .objectMapper(objectMapper) - .build(); - } +// @Bean +// @Primary +// public Serializer defaultSerializer(ObjectMapper objectMapper) { +// objectMapper.registerModule(dwellingIdSerializationModule()); +// return JacksonSerializer.builder() +// .objectMapper(objectMapper) +// .build(); +// } } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index e7c9f30..71f7b8e 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -19,6 +19,10 @@ spring: axon: axonserver: enabled: true + serializer: + general: jackson + events: jackson + messages: jackson eventhandling: processors: Automation_WhenCreatureRecruitedThenAddToArmy_Processor: From 1e28af5a0b55e8f6beb1769da6cd8360d8ff8602 Mon Sep 17 00:00:00 2001 From: Mateusz Nowak Date: Tue, 8 Jul 2025 12:19:27 +0200 Subject: [PATCH 05/11] add jpa event store config --- README.md | 5 +++++ .../creaturerecruitment/write/Dwelling.java | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/README.md b/README.md index 2f3956a..cb02464 100644 --- a/README.md +++ b/README.md @@ -158,3 +158,8 @@ void givenDwellingWith2Creatures_WhenRecruit2Creatures_ThenRecruited() { If you'd like to hire me for Domain-Driven Design and/or Event Sourcing projects I'm available to work with: Kotlin, Java, C# .NET, Ruby and JavaScript/TypeScript (Node.js or React). Please reach me out on LinkedIn [linkedin.com/in/mateusznakodach/](https://www.linkedin.com/in/mateusznakodach/). + + +### Helpful: +CHECK snapshot content! +```TO read snapshot data: SELECT lo_get(18333) AS payload;``` \ No newline at end of file diff --git a/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/write/Dwelling.java b/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/write/Dwelling.java index c440120..483330e 100644 --- a/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/write/Dwelling.java +++ b/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/write/Dwelling.java @@ -19,12 +19,16 @@ import org.axonframework.modelling.command.AggregateIdentifier; import org.axonframework.modelling.command.CreationPolicy; import org.axonframework.spring.stereotype.Aggregate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import static org.axonframework.modelling.command.AggregateLifecycle.*; @Aggregate(snapshotTriggerDefinition = "dwellingSnapshotTrigger") public class Dwelling { + private static final Logger logger = LoggerFactory.getLogger(Dwelling.class); + @AggregateIdentifier public DwellingId dwellingId; public CreatureId creatureId; @@ -47,6 +51,7 @@ void decide(BuildDwelling command) { @EventSourcingHandler void evolve(DwellingBuilt event) { + logger.info("🏗️ Dwelling built with ID: {}, creature type: {}", event.dwellingId(), event.creatureId()); this.dwellingId = new DwellingId(event.dwellingId()); this.creatureId = new CreatureId(event.creatureId()); this.costPerTroop = Resources.fromRaw(event.costPerTroop()); @@ -69,6 +74,8 @@ void decide(IncreaseAvailableCreatures command) { @EventSourcingHandler void evolve(AvailableCreaturesChanged event) { + logger.info("📈 Available creatures changed for dwelling {}: {} creatures now available", + event.dwellingId(), event.changedTo()); this.availableCreatures = new Amount(event.changedTo()); } @@ -104,11 +111,14 @@ void decide(RecruitCreature command) { @EventSourcingHandler void evolve(CreatureRecruited event) { + logger.info("🧙 Recruited {} creatures of type {} from dwelling {} to army {}", + event.quantity(), event.creatureId(), event.dwellingId(), event.toArmy()); // todo: consider if it's OK or RecruitCreature should cause also AvailableCreaturesChanged event this.availableCreatures = this.availableCreatures.minus(new Amount(event.quantity())); } Dwelling() { + logger.info("🏠 Creating empty Dwelling (required by Axon)"); // required by Axon } } From a6774e641085f36a9f59fde21e025e494ad8c41d Mon Sep 17 00:00:00 2001 From: Mateusz Nowak Date: Tue, 8 Jul 2025 14:11:43 +0200 Subject: [PATCH 06/11] snapshot filter involved in both --- README.md | 3 ++- .../astrologers/AstrologersConfiguration.java | 17 +++++++++++++++++ .../astrologers/write/Astrologers.java | 2 +- .../CreatureRecruitmentConfiguration.java | 9 +++++++++ .../creaturerecruitment/write/Dwelling.java | 2 +- 5 files changed, 30 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cb02464..1e5e5c1 100644 --- a/README.md +++ b/README.md @@ -162,4 +162,5 @@ Please reach me out on LinkedIn [linkedin.com/in/mateusznakodach/](https://www.l ### Helpful: CHECK snapshot content! -```TO read snapshot data: SELECT lo_get(18333) AS payload;``` \ No newline at end of file +```TO read snapshot data: SELECT lo_get(18333) AS payload;``` +```Error reading snapshot for aggregate [{}]. Reconstructing from entire event stream.``` \ No newline at end of file diff --git a/src/main/java/com/dddheroes/heroesofddd/astrologers/AstrologersConfiguration.java b/src/main/java/com/dddheroes/heroesofddd/astrologers/AstrologersConfiguration.java index ed7576c..9a9c3b3 100644 --- a/src/main/java/com/dddheroes/heroesofddd/astrologers/AstrologersConfiguration.java +++ b/src/main/java/com/dddheroes/heroesofddd/astrologers/AstrologersConfiguration.java @@ -3,6 +3,10 @@ import com.dddheroes.heroesofddd.astrologers.automation.whenweekstartedthenproclaimweeksymbol.WeekSymbolCalculator; import com.dddheroes.heroesofddd.astrologers.write.WeekSymbol; import com.dddheroes.heroesofddd.shared.domain.identifiers.CreatureId; +import org.axonframework.eventsourcing.EventCountSnapshotTriggerDefinition; +import org.axonframework.eventsourcing.SnapshotTriggerDefinition; +import org.axonframework.eventsourcing.Snapshotter; +import org.axonframework.eventsourcing.snapshotting.SnapshotFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -18,4 +22,17 @@ WeekSymbolCalculator inMemoryWeekSymbolCalculator() { private static int random(int min, int max) { return (int) (Math.random() * (max - min + 1) + min); } + + @Bean + public SnapshotFilter astrologersSnapshotFilter() { + return snapshotData -> { + // Allow all snapshots for dwellings, as they are always in the correct format + return true; + }; + } + + @Bean + SnapshotTriggerDefinition astrologersSnapshotTrigger(Snapshotter snapshotter) { + return new EventCountSnapshotTriggerDefinition(snapshotter, 5); + } } diff --git a/src/main/java/com/dddheroes/heroesofddd/astrologers/write/Astrologers.java b/src/main/java/com/dddheroes/heroesofddd/astrologers/write/Astrologers.java index d22906a..fff4100 100644 --- a/src/main/java/com/dddheroes/heroesofddd/astrologers/write/Astrologers.java +++ b/src/main/java/com/dddheroes/heroesofddd/astrologers/write/Astrologers.java @@ -12,7 +12,7 @@ import static org.axonframework.modelling.command.AggregateLifecycle.apply; -@Aggregate +@Aggregate(snapshotFilter = "astrologersSnapshotFilter", snapshotTriggerDefinition = "astrologersSnapshotTrigger") class Astrologers { @AggregateIdentifier diff --git a/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/CreatureRecruitmentConfiguration.java b/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/CreatureRecruitmentConfiguration.java index 88b0299..b26e71f 100644 --- a/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/CreatureRecruitmentConfiguration.java +++ b/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/CreatureRecruitmentConfiguration.java @@ -6,6 +6,7 @@ import org.axonframework.eventsourcing.EventCountSnapshotTriggerDefinition; import org.axonframework.eventsourcing.SnapshotTriggerDefinition; import org.axonframework.eventsourcing.Snapshotter; +import org.axonframework.eventsourcing.snapshotting.SnapshotFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -31,4 +32,12 @@ public Class supportedCommandType() { SnapshotTriggerDefinition dwellingSnapshotTrigger(Snapshotter snapshotter) { return new EventCountSnapshotTriggerDefinition(snapshotter, 5); } + + @Bean + public SnapshotFilter dwellingSnapshotFilter() { + return snapshotData -> { + // Allow all snapshots for dwellings, as they are always in the correct format + return true; + }; + } } diff --git a/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/write/Dwelling.java b/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/write/Dwelling.java index 483330e..74e4340 100644 --- a/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/write/Dwelling.java +++ b/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/write/Dwelling.java @@ -24,7 +24,7 @@ import static org.axonframework.modelling.command.AggregateLifecycle.*; -@Aggregate(snapshotTriggerDefinition = "dwellingSnapshotTrigger") +@Aggregate(snapshotTriggerDefinition = "dwellingSnapshotTrigger", snapshotFilter = "dwellingSnapshotFilter") public class Dwelling { private static final Logger logger = LoggerFactory.getLogger(Dwelling.class); From 3a771f5a674e874958e2217abb4e6e8d95561649 Mon Sep 17 00:00:00 2001 From: Mateusz Nowak Date: Tue, 8 Jul 2025 14:14:15 +0200 Subject: [PATCH 07/11] snapshot filter misused --- .../heroesofddd/creaturerecruitment/write/Dwelling.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/write/Dwelling.java b/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/write/Dwelling.java index 74e4340..483330e 100644 --- a/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/write/Dwelling.java +++ b/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/write/Dwelling.java @@ -24,7 +24,7 @@ import static org.axonframework.modelling.command.AggregateLifecycle.*; -@Aggregate(snapshotTriggerDefinition = "dwellingSnapshotTrigger", snapshotFilter = "dwellingSnapshotFilter") +@Aggregate(snapshotTriggerDefinition = "dwellingSnapshotTrigger") public class Dwelling { private static final Logger logger = LoggerFactory.getLogger(Dwelling.class); From beab2344e7e62c62dbb36d21c9809d080142a1be Mon Sep 17 00:00:00 2001 From: Mateusz Nowak Date: Tue, 8 Jul 2025 16:35:32 +0200 Subject: [PATCH 08/11] snapshot filters experiments --- .../heroesofddd/astrologers/AstrologersConfiguration.java | 1 + .../creaturerecruitment/CreatureRecruitmentConfiguration.java | 2 +- .../heroesofddd/creaturerecruitment/write/Dwelling.java | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/dddheroes/heroesofddd/astrologers/AstrologersConfiguration.java b/src/main/java/com/dddheroes/heroesofddd/astrologers/AstrologersConfiguration.java index 9a9c3b3..8612545 100644 --- a/src/main/java/com/dddheroes/heroesofddd/astrologers/AstrologersConfiguration.java +++ b/src/main/java/com/dddheroes/heroesofddd/astrologers/AstrologersConfiguration.java @@ -26,6 +26,7 @@ private static int random(int min, int max) { @Bean public SnapshotFilter astrologersSnapshotFilter() { return snapshotData -> { + var type = snapshotData.getType(); // Allow all snapshots for dwellings, as they are always in the correct format return true; }; diff --git a/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/CreatureRecruitmentConfiguration.java b/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/CreatureRecruitmentConfiguration.java index b26e71f..1685bfa 100644 --- a/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/CreatureRecruitmentConfiguration.java +++ b/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/CreatureRecruitmentConfiguration.java @@ -36,7 +36,7 @@ SnapshotTriggerDefinition dwellingSnapshotTrigger(Snapshotter snapshotter) { @Bean public SnapshotFilter dwellingSnapshotFilter() { return snapshotData -> { - // Allow all snapshots for dwellings, as they are always in the correct format + var type = snapshotData.getType(); return true; }; } diff --git a/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/write/Dwelling.java b/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/write/Dwelling.java index 483330e..74e4340 100644 --- a/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/write/Dwelling.java +++ b/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/write/Dwelling.java @@ -24,7 +24,7 @@ import static org.axonframework.modelling.command.AggregateLifecycle.*; -@Aggregate(snapshotTriggerDefinition = "dwellingSnapshotTrigger") +@Aggregate(snapshotTriggerDefinition = "dwellingSnapshotTrigger", snapshotFilter = "dwellingSnapshotFilter") public class Dwelling { private static final Logger logger = LoggerFactory.getLogger(Dwelling.class); From 6d31771db2162688fe5efbdadf80aa50ec3b5b7f Mon Sep 17 00:00:00 2001 From: Mateusz Nowak Date: Tue, 8 Jul 2025 20:07:00 +0200 Subject: [PATCH 09/11] add serializers for ids --- .cursorrules | 261 ------------------ generated-requests.http | 6 +- .../heroesofddd/GameConfiguration.java | 20 -- .../astrologers/AstrologersConfiguration.java | 18 -- .../astrologers/write/Astrologers.java | 2 +- .../CreatureRecruitmentConfiguration.java | 35 ++- .../creaturerecruitment/write/Dwelling.java | 6 +- .../SerializationConfiguration.java | 139 ++++++++++ .../DwellingIdSerializationModule.java | 35 --- 9 files changed, 173 insertions(+), 349 deletions(-) delete mode 100644 .cursorrules create mode 100644 src/main/java/com/dddheroes/heroesofddd/shared/infrastructure/SerializationConfiguration.java delete mode 100644 src/main/java/com/dddheroes/heroesofddd/shared/infrastructure/serialization/DwellingIdSerializationModule.java diff --git a/.cursorrules b/.cursorrules deleted file mode 100644 index 7c03ff3..0000000 --- a/.cursorrules +++ /dev/null @@ -1,261 +0,0 @@ -# TypeScript Event Sourcing with Emmett - Cursor Rules - -You are an expert TypeScript developer implementing Event Sourcing using the Emmett library. Follow these patterns and principles based on the reference Java/Axon implementation at `/Users/mateusznowak/GitRepos/MateuszNaKodach/HeroesOfDomainDrivenDesign.EventSourcing.Java.Axon.Spring`. - -## Architecture Patterns - -### Vertical Slice Architecture -- **Reference**: Java project modules (armies, astrologers, calendar, creaturerecruitment, resourcespool) -- Organize code by business capabilities, not technical layers -- Each slice should be self-contained with its own: - - Commands and command handlers - - Events and event handlers - - Aggregates - - Read models and projectors - - Domain rules - - REST APIs - - Configuration - -**TypeScript Structure:** -``` -src/ - slices/ - creature-recruitment/ - write/ - commands/ - aggregates/ - domain-rules/ - read/ - projectors/ - queries/ - events/ - api/ - armies/ - write/ read/ events/ api/ - calendar/ - write/ read/ events/ api/ -``` - -### Event Sourcing with Emmett - -#### Aggregates -- **Reference**: `Dwelling.java` aggregate pattern -- Use Emmett's `aggregate` function to define aggregates -- Implement command handlers that validate business rules and emit events -- Implement event handlers that evolve aggregate state -- Keep aggregates focused on a single business concept - -```typescript -// Example based on Dwelling.java -export const dwelling = aggregate( - 'Dwelling', - { - // Command handlers (equivalent to @CommandHandler methods) - buildDwelling: (command: BuildDwelling, state: DwellingState | null) => { - // Domain rule validation (like OnlyNotBuiltBuildingCanBeBuild) - if (state !== null) { - throw new DomainRuleViolation('Only not built building can be built'); - } - - return [DwellingBuilt.create(command)]; - }, - - recruitCreature: (command: RecruitCreature, state: DwellingState) => { - // Multiple domain rules validation - validateRecruitmentRules(command, state); - - return [CreatureRecruited.create(command, state)]; - } - }, - - // Event handlers (equivalent to @EventSourcingHandler methods) - { - DwellingBuilt: (state: DwellingState | null, event: DwellingBuilt) => ({ - dwellingId: event.dwellingId, - creatureId: event.creatureId, - costPerTroop: event.costPerTroop, - availableCreatures: 0, - }), - - CreatureRecruited: (state: DwellingState, event: CreatureRecruited) => ({ - ...state, - availableCreatures: state.availableCreatures - event.quantity, - }), - } -); -``` - -#### Events -- **Reference**: Event structure from `CreatureRecruited.java` and sealed interface `DwellingEvent.java` -- Use TypeScript discriminated unions for event types -- Include static factory methods for event creation -- Implement proper serialization for persistence - -```typescript -// Based on DwellingEvent.java sealed interface pattern -export type DwellingEvent = - | DwellingBuilt - | AvailableCreaturesChanged - | CreatureRecruited; - -export interface CreatureRecruited { - type: 'CreatureRecruited'; - dwellingId: string; - creatureId: string; - toArmy: string; - quantity: number; - totalCost: Resources; - - // Static factory method like in Java - static create(command: RecruitCreature, state: DwellingState): CreatureRecruited; -} -``` - -#### Domain Rules -- **Reference**: `DomainRule.java` interface and implementations like `OnlyNotBuiltBuildingCanBeBuild.java` -- Implement business rules as separate, testable classes -- Use descriptive names that express business intent -- Include proper error messages - -```typescript -// Based on DomainRule.java pattern -export interface DomainRule { - isViolated(): boolean; - message(): string; - verify(): void; // throws if violated -} - -export class RecruitCreaturesNotExceedAvailable implements DomainRule { - constructor( - private readonly available: number, - private readonly requested: number - ) {} - - isViolated(): boolean { - return this.requested > this.available; - } - - message(): string { - return `Cannot recruit ${this.requested} creatures, only ${this.available} available`; - } - - verify(): void { - if (this.isViolated()) { - throw new DomainRuleViolation(this.message()); - } - } -} -``` - -### CQRS Implementation - -#### Commands -- **Reference**: Command pattern from Java (e.g., `BuildDwelling.java`) -- Implement commands as immutable data structures -- Include validation and factory methods -- Group related commands in the same slice - -#### Read Models & Projectors -- **Reference**: `DwellingReadModelProjector.java` pattern -- Use Emmett's projection capabilities -- Create separate optimized read models -- Handle event ordering and replay scenarios - -```typescript -// Based on DwellingReadModelProjector.java -export const dwellingReadModelProjector = projector({ - name: 'DwellingReadModel', - - eventHandlers: { - DwellingBuilt: async (event: DwellingBuilt, { store }) => { - await store.upsert('dwellings', { - id: event.dwellingId, - creatureId: event.creatureId, - costPerTroop: event.costPerTroop, - availableCreatures: 0, - }); - }, - - CreatureRecruited: async (event: CreatureRecruited, { store }) => { - await store.update('dwellings', event.dwellingId, dwelling => ({ - ...dwelling, - availableCreatures: dwelling.availableCreatures - event.quantity, - })); - }, - } -}); -``` - -### Process Managers (Sagas) -- **Reference**: `WhenCreatureRecruitedThenAddToArmyProcessor.java` -- Handle cross-aggregate coordination -- Implement compensation logic for failed operations -- Use descriptive naming that expresses the business process - -```typescript -// Based on WhenCreatureRecruitedThenAddToArmyProcessor.java -export const creatureRecruitmentSaga = saga({ - name: 'CreatureRecruitmentAutomation', - - eventHandlers: { - CreatureRecruited: async (event: CreatureRecruited, { commandBus }) => { - try { - await commandBus.send(AddCreatureToArmy.create( - event.toArmy, - event.creatureId, - event.quantity - )); - } catch (error) { - // Compensation action - await commandBus.send(IncreaseAvailableCreatures.create( - event.dwellingId, - event.creatureId, - event.quantity - )); - } - } - } -}); -``` - -## Code Organization Principles - -### File Structure -Follow the Java reference structure but adapted for TypeScript: -- Group by business capability (vertical slices) -- Separate write/read sides clearly -- Keep domain rules in dedicated files -- Co-locate related components - -### Naming Conventions -- **Reference**: Consistent naming from Java codebase -- Use descriptive, business-focused names -- Commands: imperative (BuildDwelling, RecruitCreature) -- Events: past tense (DwellingBuilt, CreatureRecruited) -- Domain Rules: express business constraints clearly - -### Testing Strategy -- **Reference**: Test structure from Java codebase -- Test aggregates in isolation -- Test domain rules separately -- Test projectors with event sequences -- Use descriptive test names that express business scenarios - -## Best Practices - -1. **Event Store as Source of Truth**: Like in the Java reference, treat events as the authoritative data source -2. **Aggregate Boundaries**: Keep aggregates small and focused, like Dwelling aggregate -3. **Cross-Aggregate Communication**: Use events and process managers, never direct calls -4. **Domain Language**: Use ubiquitous language consistently (creatures, dwellings, armies) -5. **Immutability**: Ensure events and commands are immutable -6. **Error Handling**: Implement proper domain exceptions and compensation actions - -## Configuration and Setup - -Set up Emmett with similar patterns to the Java Spring configuration: -- Configure event store connection -- Set up projection processing groups -- Implement command cost resolution (like `CommandCostResolver`) -- Configure cross-cutting concerns (metadata, tracing) - -Remember: This reference Java/Axon implementation shows mature Event Sourcing patterns. Adapt these concepts to Emmett's API while maintaining the same architectural principles and business logic organization. \ No newline at end of file diff --git a/generated-requests.http b/generated-requests.http index 9627b58..2c3fc32 100644 --- a/generated-requests.http +++ b/generated-requests.http @@ -1,7 +1,7 @@ ### BuildDwelling -@gameId = scenario-3 -@playerId = player-3 -@dwellingId = dwelling-5 +@gameId = scenario-1 +@playerId = player-1 +@dwellingId = dwelling-1 ### BuildDwelling PUT http://localhost:8080/games/{{gameId}}/dwellings/{{dwellingId}} diff --git a/src/main/java/com/dddheroes/heroesofddd/GameConfiguration.java b/src/main/java/com/dddheroes/heroesofddd/GameConfiguration.java index b5d3264..383bc64 100644 --- a/src/main/java/com/dddheroes/heroesofddd/GameConfiguration.java +++ b/src/main/java/com/dddheroes/heroesofddd/GameConfiguration.java @@ -1,19 +1,13 @@ package com.dddheroes.heroesofddd; import com.dddheroes.heroesofddd.shared.application.GameMetaData; -import com.dddheroes.heroesofddd.shared.infrastructure.serialization.DwellingIdSerializationModule; -import com.fasterxml.jackson.databind.Module; -import com.fasterxml.jackson.databind.ObjectMapper; import org.axonframework.eventhandling.EventMessage; import org.axonframework.eventhandling.async.SequencingPolicy; import org.axonframework.messaging.correlation.CorrelationDataProvider; import org.axonframework.messaging.correlation.MessageOriginProvider; import org.axonframework.messaging.correlation.SimpleCorrelationDataProvider; -import org.axonframework.serialization.Serializer; -import org.axonframework.serialization.json.JacksonSerializer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; @Configuration public class GameConfiguration { @@ -32,18 +26,4 @@ public CorrelationDataProvider gameDataProvider() { public CorrelationDataProvider messageOriginProvider() { return new MessageOriginProvider(); } - - @Bean - public Module dwellingIdSerializationModule() { - return new DwellingIdSerializationModule(); - } - -// @Bean -// @Primary -// public Serializer defaultSerializer(ObjectMapper objectMapper) { -// objectMapper.registerModule(dwellingIdSerializationModule()); -// return JacksonSerializer.builder() -// .objectMapper(objectMapper) -// .build(); -// } } diff --git a/src/main/java/com/dddheroes/heroesofddd/astrologers/AstrologersConfiguration.java b/src/main/java/com/dddheroes/heroesofddd/astrologers/AstrologersConfiguration.java index 8612545..ed7576c 100644 --- a/src/main/java/com/dddheroes/heroesofddd/astrologers/AstrologersConfiguration.java +++ b/src/main/java/com/dddheroes/heroesofddd/astrologers/AstrologersConfiguration.java @@ -3,10 +3,6 @@ import com.dddheroes.heroesofddd.astrologers.automation.whenweekstartedthenproclaimweeksymbol.WeekSymbolCalculator; import com.dddheroes.heroesofddd.astrologers.write.WeekSymbol; import com.dddheroes.heroesofddd.shared.domain.identifiers.CreatureId; -import org.axonframework.eventsourcing.EventCountSnapshotTriggerDefinition; -import org.axonframework.eventsourcing.SnapshotTriggerDefinition; -import org.axonframework.eventsourcing.Snapshotter; -import org.axonframework.eventsourcing.snapshotting.SnapshotFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -22,18 +18,4 @@ WeekSymbolCalculator inMemoryWeekSymbolCalculator() { private static int random(int min, int max) { return (int) (Math.random() * (max - min + 1) + min); } - - @Bean - public SnapshotFilter astrologersSnapshotFilter() { - return snapshotData -> { - var type = snapshotData.getType(); - // Allow all snapshots for dwellings, as they are always in the correct format - return true; - }; - } - - @Bean - SnapshotTriggerDefinition astrologersSnapshotTrigger(Snapshotter snapshotter) { - return new EventCountSnapshotTriggerDefinition(snapshotter, 5); - } } diff --git a/src/main/java/com/dddheroes/heroesofddd/astrologers/write/Astrologers.java b/src/main/java/com/dddheroes/heroesofddd/astrologers/write/Astrologers.java index fff4100..d22906a 100644 --- a/src/main/java/com/dddheroes/heroesofddd/astrologers/write/Astrologers.java +++ b/src/main/java/com/dddheroes/heroesofddd/astrologers/write/Astrologers.java @@ -12,7 +12,7 @@ import static org.axonframework.modelling.command.AggregateLifecycle.apply; -@Aggregate(snapshotFilter = "astrologersSnapshotFilter", snapshotTriggerDefinition = "astrologersSnapshotTrigger") +@Aggregate class Astrologers { @AggregateIdentifier diff --git a/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/CreatureRecruitmentConfiguration.java b/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/CreatureRecruitmentConfiguration.java index 1685bfa..9c1bcdc 100644 --- a/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/CreatureRecruitmentConfiguration.java +++ b/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/CreatureRecruitmentConfiguration.java @@ -1,15 +1,22 @@ package com.dddheroes.heroesofddd.creaturerecruitment; +import com.dddheroes.heroesofddd.creaturerecruitment.write.DwellingId; import com.dddheroes.heroesofddd.creaturerecruitment.write.recruitcreature.RecruitCreature; import com.dddheroes.heroesofddd.resourcespool.application.CommandCostResolver; import com.dddheroes.heroesofddd.shared.domain.valueobjects.Resources; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.module.SimpleModule; import org.axonframework.eventsourcing.EventCountSnapshotTriggerDefinition; import org.axonframework.eventsourcing.SnapshotTriggerDefinition; import org.axonframework.eventsourcing.Snapshotter; -import org.axonframework.eventsourcing.snapshotting.SnapshotFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import java.io.IOException; + @Configuration class CreatureRecruitmentConfiguration { @@ -34,10 +41,26 @@ SnapshotTriggerDefinition dwellingSnapshotTrigger(Snapshotter snapshotter) { } @Bean - public SnapshotFilter dwellingSnapshotFilter() { - return snapshotData -> { - var type = snapshotData.getType(); - return true; - }; + public Module dwellingIdSerializationModule() { + return new DwellingIdSerializationModule(); + } + + private static class DwellingIdSerializationModule extends SimpleModule { + + public DwellingIdSerializationModule() { + addSerializer(DwellingId.class, new JsonSerializer<>() { + @Override + public void serialize(DwellingId value, JsonGenerator gen, SerializerProvider __) throws IOException { + gen.writeString(value.raw()); + } + }); + addDeserializer(DwellingId.class, new JsonDeserializer<>() { + @Override + public DwellingId deserialize(JsonParser p, DeserializationContext __) throws IOException { + return new DwellingId(p.getValueAsString()); + } + }); + } + } } diff --git a/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/write/Dwelling.java b/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/write/Dwelling.java index 74e4340..df2a508 100644 --- a/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/write/Dwelling.java +++ b/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/write/Dwelling.java @@ -24,7 +24,7 @@ import static org.axonframework.modelling.command.AggregateLifecycle.*; -@Aggregate(snapshotTriggerDefinition = "dwellingSnapshotTrigger", snapshotFilter = "dwellingSnapshotFilter") +@Aggregate(snapshotTriggerDefinition = "dwellingSnapshotTrigger") public class Dwelling { private static final Logger logger = LoggerFactory.getLogger(Dwelling.class); @@ -80,11 +80,7 @@ void evolve(AvailableCreaturesChanged event) { } @CommandHandler -// @CreationPolicy(AggregateCreationPolicy.CREATE_IF_MISSING) void decide(RecruitCreature command) { -// if(dwellingId == null){ -// throw new DomainRule.ViolatedException("Only not built building can be build"); -// } new RecruitCreaturesNotExceedAvailableCreatures( creatureId, availableCreatures, diff --git a/src/main/java/com/dddheroes/heroesofddd/shared/infrastructure/SerializationConfiguration.java b/src/main/java/com/dddheroes/heroesofddd/shared/infrastructure/SerializationConfiguration.java new file mode 100644 index 0000000..9ba57be --- /dev/null +++ b/src/main/java/com/dddheroes/heroesofddd/shared/infrastructure/SerializationConfiguration.java @@ -0,0 +1,139 @@ +package com.dddheroes.heroesofddd.shared.infrastructure; + +import com.dddheroes.heroesofddd.shared.domain.identifiers.ArmyId; +import com.dddheroes.heroesofddd.shared.domain.identifiers.CreatureId; +import com.dddheroes.heroesofddd.shared.domain.identifiers.GameId; +import com.dddheroes.heroesofddd.shared.domain.identifiers.PlayerId; +import com.dddheroes.heroesofddd.shared.domain.valueobjects.Amount; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.module.SimpleModule; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.io.IOException; + +@Configuration +public class SerializationConfiguration { + + @Bean + public Module gameIdSerializationModule() { + return new GameIdSerializationModule(); + } + + @Bean + public Module playerIdSerializationModule() { + return new PlayerIdSerializationModule(); + } + + @Bean + public Module creatureIdSerializationModule() { + return new CreatureIdSerializationModule(); + } + + @Bean + public Module armyIdSerializationModule() { + return new ArmyIdSerializationModule(); + } + + @Bean + public Module amountSerializationModule() { + return new AmountSerializationModule(); + } + + private static class GameIdSerializationModule extends SimpleModule { + + public GameIdSerializationModule() { + addSerializer(GameId.class, new JsonSerializer<>() { + @Override + public void serialize(GameId value, JsonGenerator gen, SerializerProvider __) throws IOException { + gen.writeString(value.raw()); + } + }); + addDeserializer(GameId.class, new JsonDeserializer<>() { + @Override + public GameId deserialize(JsonParser p, DeserializationContext __) throws IOException { + return new GameId(p.getValueAsString()); + } + }); + } + + } + + private static class PlayerIdSerializationModule extends SimpleModule { + + public PlayerIdSerializationModule() { + addSerializer(PlayerId.class, new JsonSerializer<>() { + @Override + public void serialize(PlayerId value, JsonGenerator gen, SerializerProvider __) throws IOException { + gen.writeString(value.raw()); + } + }); + addDeserializer(PlayerId.class, new JsonDeserializer<>() { + @Override + public PlayerId deserialize(JsonParser p, DeserializationContext __) throws IOException { + return new PlayerId(p.getValueAsString()); + } + }); + } + } + + private static class CreatureIdSerializationModule extends SimpleModule { + + public CreatureIdSerializationModule() { + addSerializer(CreatureId.class, new JsonSerializer<>() { + @Override + public void serialize(CreatureId value, JsonGenerator gen, SerializerProvider __) throws IOException { + gen.writeString(value.raw()); + } + }); + addDeserializer(CreatureId.class, new JsonDeserializer<>() { + @Override + public CreatureId deserialize(JsonParser p, DeserializationContext __) throws IOException { + return new CreatureId(p.getValueAsString()); + } + }); + } + + } + + private static class ArmyIdSerializationModule extends SimpleModule { + + public ArmyIdSerializationModule() { + addSerializer(ArmyId.class, new JsonSerializer<>() { + @Override + public void serialize(ArmyId value, JsonGenerator gen, SerializerProvider __) throws IOException { + gen.writeString(value.raw()); + } + }); + addDeserializer(ArmyId.class, new JsonDeserializer<>() { + @Override + public ArmyId deserialize(JsonParser p, DeserializationContext __) throws IOException { + return new ArmyId(p.getValueAsString()); + } + }); + } + + } + + private static class AmountSerializationModule extends SimpleModule { + + public AmountSerializationModule() { + addSerializer(Amount.class, new JsonSerializer<>() { + @Override + public void serialize(Amount value, JsonGenerator gen, SerializerProvider __) throws IOException { + gen.writeNumber(value.raw()); + } + }); + addDeserializer(Amount.class, new JsonDeserializer<>() { + @Override + public Amount deserialize(JsonParser p, DeserializationContext __) throws IOException { + return new Amount(p.getValueAsInt()); + } + }); + } + + } +} diff --git a/src/main/java/com/dddheroes/heroesofddd/shared/infrastructure/serialization/DwellingIdSerializationModule.java b/src/main/java/com/dddheroes/heroesofddd/shared/infrastructure/serialization/DwellingIdSerializationModule.java deleted file mode 100644 index 43f865a..0000000 --- a/src/main/java/com/dddheroes/heroesofddd/shared/infrastructure/serialization/DwellingIdSerializationModule.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.dddheroes.heroesofddd.shared.infrastructure.serialization; - -import com.dddheroes.heroesofddd.creaturerecruitment.write.DwellingId; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.module.SimpleModule; - -import java.io.IOException; - -public class DwellingIdSerializationModule extends SimpleModule { - - public DwellingIdSerializationModule() { - addSerializer(DwellingId.class, new DwellingIdSerializer()); - addDeserializer(DwellingId.class, new DwellingIdDeserializer()); - } - - private static class DwellingIdSerializer extends JsonSerializer { - @Override - public void serialize(DwellingId value, JsonGenerator gen, SerializerProvider serializers) throws IOException { - gen.writeString(value.raw()); - } - } - - private static class DwellingIdDeserializer extends JsonDeserializer { - @Override - public DwellingId deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - String value = p.getValueAsString(); - return DwellingId.of(value); - } - } -} From 60345f93fd70edc559507fb3534b2c5b7446e388 Mon Sep 17 00:00:00 2001 From: Mateusz Nowak Date: Tue, 8 Jul 2025 20:12:26 +0200 Subject: [PATCH 10/11] add serializers for ids --- .../heroesofddd/creaturerecruitment/write/Dwelling.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/write/Dwelling.java b/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/write/Dwelling.java index df2a508..fd0c495 100644 --- a/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/write/Dwelling.java +++ b/src/main/java/com/dddheroes/heroesofddd/creaturerecruitment/write/Dwelling.java @@ -30,7 +30,7 @@ public class Dwelling { private static final Logger logger = LoggerFactory.getLogger(Dwelling.class); @AggregateIdentifier - public DwellingId dwellingId; + public DwellingId dwellingId; // needs to be public for snapshotting public CreatureId creatureId; public Resources costPerTroop; public Amount availableCreatures; @@ -114,7 +114,9 @@ void evolve(CreatureRecruited event) { } Dwelling() { - logger.info("🏠 Creating empty Dwelling (required by Axon)"); + logger.info("\uD83D\uDC80 Dwelling non-args constructor"); // required by Axon } + + } From 02df96566d4c2c77738421636250e7eb75ad3ed2 Mon Sep 17 00:00:00 2001 From: Mateusz Nowak Date: Tue, 8 Jul 2025 20:12:58 +0200 Subject: [PATCH 11/11] add serializers for ids --- README.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/README.md b/README.md index 1e5e5c1..f5735b6 100644 --- a/README.md +++ b/README.md @@ -157,10 +157,4 @@ void givenDwellingWith2Creatures_WhenRecruit2Creatures_ThenRecruited() { If you'd like to hire me for Domain-Driven Design and/or Event Sourcing projects I'm available to work with: Kotlin, Java, C# .NET, Ruby and JavaScript/TypeScript (Node.js or React). -Please reach me out on LinkedIn [linkedin.com/in/mateusznakodach/](https://www.linkedin.com/in/mateusznakodach/). - - -### Helpful: -CHECK snapshot content! -```TO read snapshot data: SELECT lo_get(18333) AS payload;``` -```Error reading snapshot for aggregate [{}]. Reconstructing from entire event stream.``` \ No newline at end of file +Please reach me out on LinkedIn [linkedin.com/in/mateusznakodach/](https://www.linkedin.com/in/mateusznakodach/). \ No newline at end of file