1+ # TypeScript Event Sourcing with Emmett - Cursor Rules
2+
3+ 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`.
4+
5+ ## Architecture Patterns
6+
7+ ### Vertical Slice Architecture
8+ - **Reference**: Java project modules (armies, astrologers, calendar, creaturerecruitment, resourcespool)
9+ - Organize code by business capabilities, not technical layers
10+ - Each slice should be self-contained with its own:
11+ - Commands and command handlers
12+ - Events and event handlers
13+ - Aggregates
14+ - Read models and projectors
15+ - Domain rules
16+ - REST APIs
17+ - Configuration
18+
19+ **TypeScript Structure:**
20+ ```
21+ src/
22+ slices/
23+ creature-recruitment/
24+ write/
25+ commands/
26+ aggregates/
27+ domain-rules/
28+ read/
29+ projectors/
30+ queries/
31+ events/
32+ api/
33+ armies/
34+ write/ read/ events/ api/
35+ calendar/
36+ write/ read/ events/ api/
37+ ```
38+
39+ ### Event Sourcing with Emmett
40+
41+ #### Aggregates
42+ - **Reference**: `Dwelling.java` aggregate pattern
43+ - Use Emmett's `aggregate` function to define aggregates
44+ - Implement command handlers that validate business rules and emit events
45+ - Implement event handlers that evolve aggregate state
46+ - Keep aggregates focused on a single business concept
47+
48+ ```typescript
49+ // Example based on Dwelling.java
50+ export const dwelling = aggregate<DwellingState, DwellingEvent>(
51+ 'Dwelling',
52+ {
53+ // Command handlers (equivalent to @CommandHandler methods)
54+ buildDwelling: (command: BuildDwelling, state: DwellingState | null) => {
55+ // Domain rule validation (like OnlyNotBuiltBuildingCanBeBuild)
56+ if (state !== null) {
57+ throw new DomainRuleViolation('Only not built building can be built');
58+ }
59+
60+ return [DwellingBuilt.create(command)];
61+ },
62+
63+ recruitCreature: (command: RecruitCreature, state: DwellingState) => {
64+ // Multiple domain rules validation
65+ validateRecruitmentRules(command, state);
66+
67+ return [CreatureRecruited.create(command, state)];
68+ }
69+ },
70+
71+ // Event handlers (equivalent to @EventSourcingHandler methods)
72+ {
73+ DwellingBuilt: (state: DwellingState | null, event: DwellingBuilt) => ({
74+ dwellingId: event.dwellingId,
75+ creatureId: event.creatureId,
76+ costPerTroop: event.costPerTroop,
77+ availableCreatures: 0,
78+ }),
79+
80+ CreatureRecruited: (state: DwellingState, event: CreatureRecruited) => ({
81+ ...state,
82+ availableCreatures: state.availableCreatures - event.quantity,
83+ }),
84+ }
85+ );
86+ ```
87+
88+ #### Events
89+ - **Reference**: Event structure from `CreatureRecruited.java` and sealed interface `DwellingEvent.java`
90+ - Use TypeScript discriminated unions for event types
91+ - Include static factory methods for event creation
92+ - Implement proper serialization for persistence
93+
94+ ```typescript
95+ // Based on DwellingEvent.java sealed interface pattern
96+ export type DwellingEvent =
97+ | DwellingBuilt
98+ | AvailableCreaturesChanged
99+ | CreatureRecruited;
100+
101+ export interface CreatureRecruited {
102+ type: 'CreatureRecruited';
103+ dwellingId: string;
104+ creatureId: string;
105+ toArmy: string;
106+ quantity: number;
107+ totalCost: Resources;
108+
109+ // Static factory method like in Java
110+ static create(command: RecruitCreature, state: DwellingState): CreatureRecruited;
111+ }
112+ ```
113+
114+ #### Domain Rules
115+ - **Reference**: `DomainRule.java` interface and implementations like `OnlyNotBuiltBuildingCanBeBuild.java`
116+ - Implement business rules as separate, testable classes
117+ - Use descriptive names that express business intent
118+ - Include proper error messages
119+
120+ ```typescript
121+ // Based on DomainRule.java pattern
122+ export interface DomainRule {
123+ isViolated(): boolean;
124+ message(): string;
125+ verify(): void; // throws if violated
126+ }
127+
128+ export class RecruitCreaturesNotExceedAvailable implements DomainRule {
129+ constructor(
130+ private readonly available: number,
131+ private readonly requested: number
132+ ) {}
133+
134+ isViolated(): boolean {
135+ return this.requested > this.available;
136+ }
137+
138+ message(): string {
139+ return `Cannot recruit ${this.requested} creatures, only ${this.available} available`;
140+ }
141+
142+ verify(): void {
143+ if (this.isViolated()) {
144+ throw new DomainRuleViolation(this.message());
145+ }
146+ }
147+ }
148+ ```
149+
150+ ### CQRS Implementation
151+
152+ #### Commands
153+ - **Reference**: Command pattern from Java (e.g., `BuildDwelling.java`)
154+ - Implement commands as immutable data structures
155+ - Include validation and factory methods
156+ - Group related commands in the same slice
157+
158+ #### Read Models & Projectors
159+ - **Reference**: `DwellingReadModelProjector.java` pattern
160+ - Use Emmett's projection capabilities
161+ - Create separate optimized read models
162+ - Handle event ordering and replay scenarios
163+
164+ ```typescript
165+ // Based on DwellingReadModelProjector.java
166+ export const dwellingReadModelProjector = projector({
167+ name: 'DwellingReadModel',
168+
169+ eventHandlers: {
170+ DwellingBuilt: async (event: DwellingBuilt, { store }) => {
171+ await store.upsert('dwellings', {
172+ id: event.dwellingId,
173+ creatureId: event.creatureId,
174+ costPerTroop: event.costPerTroop,
175+ availableCreatures: 0,
176+ });
177+ },
178+
179+ CreatureRecruited: async (event: CreatureRecruited, { store }) => {
180+ await store.update('dwellings', event.dwellingId, dwelling => ({
181+ ...dwelling,
182+ availableCreatures: dwelling.availableCreatures - event.quantity,
183+ }));
184+ },
185+ }
186+ });
187+ ```
188+
189+ ### Process Managers (Sagas)
190+ - **Reference**: `WhenCreatureRecruitedThenAddToArmyProcessor.java`
191+ - Handle cross-aggregate coordination
192+ - Implement compensation logic for failed operations
193+ - Use descriptive naming that expresses the business process
194+
195+ ```typescript
196+ // Based on WhenCreatureRecruitedThenAddToArmyProcessor.java
197+ export const creatureRecruitmentSaga = saga({
198+ name: 'CreatureRecruitmentAutomation',
199+
200+ eventHandlers: {
201+ CreatureRecruited: async (event: CreatureRecruited, { commandBus }) => {
202+ try {
203+ await commandBus.send(AddCreatureToArmy.create(
204+ event.toArmy,
205+ event.creatureId,
206+ event.quantity
207+ ));
208+ } catch (error) {
209+ // Compensation action
210+ await commandBus.send(IncreaseAvailableCreatures.create(
211+ event.dwellingId,
212+ event.creatureId,
213+ event.quantity
214+ ));
215+ }
216+ }
217+ }
218+ });
219+ ```
220+
221+ ## Code Organization Principles
222+
223+ ### File Structure
224+ Follow the Java reference structure but adapted for TypeScript:
225+ - Group by business capability (vertical slices)
226+ - Separate write/read sides clearly
227+ - Keep domain rules in dedicated files
228+ - Co-locate related components
229+
230+ ### Naming Conventions
231+ - **Reference**: Consistent naming from Java codebase
232+ - Use descriptive, business-focused names
233+ - Commands: imperative (BuildDwelling, RecruitCreature)
234+ - Events: past tense (DwellingBuilt, CreatureRecruited)
235+ - Domain Rules: express business constraints clearly
236+
237+ ### Testing Strategy
238+ - **Reference**: Test structure from Java codebase
239+ - Test aggregates in isolation
240+ - Test domain rules separately
241+ - Test projectors with event sequences
242+ - Use descriptive test names that express business scenarios
243+
244+ ## Best Practices
245+
246+ 1. **Event Store as Source of Truth**: Like in the Java reference, treat events as the authoritative data source
247+ 2. **Aggregate Boundaries**: Keep aggregates small and focused, like Dwelling aggregate
248+ 3. **Cross-Aggregate Communication**: Use events and process managers, never direct calls
249+ 4. **Domain Language**: Use ubiquitous language consistently (creatures, dwellings, armies)
250+ 5. **Immutability**: Ensure events and commands are immutable
251+ 6. **Error Handling**: Implement proper domain exceptions and compensation actions
252+
253+ ## Configuration and Setup
254+
255+ Set up Emmett with similar patterns to the Java Spring configuration:
256+ - Configure event store connection
257+ - Set up projection processing groups
258+ - Implement command cost resolution (like `CommandCostResolver`)
259+ - Configure cross-cutting concerns (metadata, tracing)
260+
261+ 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.
0 commit comments