Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- ---
- **Title:**
- How to handle intermediate state transitions in command handlers in an event-driven architecture?
- ---
- **Body:**
- I'm building a game-style application using an event-sourced-ish architecture. Here's the basic structure:
- - A **dispatcher** on the game engine receives commands and routes them to command handlers.
- - Each handler returns a list of **events** based on current state and command input. **Handlers never update state directly** — they return events only.
- - The dispatcher passes all events to a pure reducer which mutates the state.
- - The dispatcher emits the event.
- - Client uses the same **reducer** to apply events to state.
- Here's what the core dispatcher looks like:
- ```ts
- public executeCommand(command: Command) {
- try {
- const events = this.handleCommand(command);
- events.forEach((event) => {
- this.state = applyEvent(this.state, event);
- this.emitEvent(event);
- });
- } catch (error) {
- this.emitError(error);
- }
- }
- private handleCommand(command: Command): GameEvent[] {
- const handler = this.commandHandlers[command.type];
- if (!handler) {
- throw new Error(`Unknown command: ${command.type}`);
- }
- const ctx = new GameContext(this.state);
- return handler(ctx, command as any);
- }
- ```
- This setup has been nice so far. Until...
- ---
- ### 🧩 The Problem: Logic that depends on intermediate state
- Here's where the design gets tricky. Some commands involve logic that depends on how **earlier events** from the same command.
- #### Example: Chain Shot
- ```txt
- Command: Player uses "Chain Shot" on Enemy A
- → Emit DamageDealt to Enemy A
- → If A dies, retarget to a random living enemy
- → Emit DamageDealt to Enemy B
- → If B dies, repeat again (up to 3 times)
- ```
- The problem:
- Whether to retarget depends on which enemies are still alive — which depends on **previous events in the same handler**. But the reducer hasn’t been called yet, so the handler can’t observe that state directly.
- ---
- ### 🤔 Architectural Tension
- To make this work, I’m weighing two approaches:
- ---
- #### ✅ Option 1: Apply events to a draft state inside the handler
- The handler simulates the reducer locally to compute intermediate results.
- ```ts
- const draft = clone(state);
- const events: Event[] = [];
- const e1 = DamageDealt(enemyA.id, 30);
- applyEvent(draft, e1);
- events.push(e1);
- if (draft.enemies[enemyA.id].hp <= 0) {
- const target = chooseRandomAliveEnemy(draft);
- const e2 = DamageDealt(target.id, 30);
- applyEvent(draft, e2);
- events.push(e2);
- }
- ```
- 🟢 Pros:
- - Leverages reducer logic, so behavior is consistent
- - Keeps events granular and observable
- 🔴 Cons:
- - Feels like im kind of undermining the point of my architecture?
- ---
- #### ✅ Option 2: Predict outcome manually in handler
- Instead of simulating state changes, the handler infers state by redoing the logic manually.
- ```ts
- const events: Event[] = [];
- const dmg = 30;
- const remainingHp = enemyA.hp - dmg;
- events.push(DamageDealt(enemyA.id, dmg));
- if (remainingHp <= 0) {
- const living = enemies.filter((e) => e.id !== enemyA.id && e.hp > 0);
- const target = chooseRandom(living);
- events.push(DamageDealt(target.id, dmg));
- }
- ```
- 🟢 Pros:
- - Keeps handler and reducer fully decoupled
- - No internal state simulation
- 🔴 Cons:
- - Duplicates reducer logic
- - Risk of logic drift or inconsistency.
- ---
- ### ❓What I'm asking
- - Is using the reducer on a draft state in the handler an acceptable and clean approach?
- - Is this a red flag for my architecture? Is there a better pattern I’m missing?
- - Have others run into this when designing game systems or simulations with event-driven patterns?
- - Any feedback on the design or long-term risks I should be aware of?
- I’m trying to balance clean separation of concerns with practical game logic. Would love to hear how others have tackled this.
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement