Skip to content

Commit 3d52dbf

Browse files
add jpa event store config
1 parent 599ec75 commit 3d52dbf

3 files changed

Lines changed: 273 additions & 1 deletion

File tree

.cursorrules

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
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.

generated-requests.http

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
### BuildDwelling
22
@gameId = scenario-3
33
@playerId = player-3
4-
@dwellingId = dwelling-3
4+
@dwellingId = dwelling-4
55

66
### BuildDwelling
77
PUT http://localhost:8080/games/{{gameId}}/dwellings/{{dwellingId}}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
axon:
2+
axonserver:
3+
enabled: false
4+
# eventhandling:
5+
# processors:
6+
# (Optional) You can override processor settings here if needed
7+
# eventstore:
8+
# No need to specify storage-engine if axon-server is disabled and JPA is on classpath
9+
# Axon will auto-configure JpaEventStorageEngine
10+
11+
# Inherit datasource and JPA config from application.yaml

0 commit comments

Comments
 (0)