Mutation Tracking Architecture
The Booking Flow Builder uses a command-driven mutation tracking system that captures all canvas interactions and aggregates them into optimized database operations on save.
Architecture Overview
Core Components
Command Stack
The command stack is the central data structure for tracking mutations:
type CommandStack = {
commands: PersistCommand[];
pointer: number; // Current position for undo/redo
};
type PersistCommand = {
id: string;
type: InteractionType;
entityType: EntityType;
entityId: string;
mutationType: 'CREATE' | 'UPDATE' | 'DELETE';
data: EntityData;
previousData?: EntityData; // For undo
relationshipContext?: RelationshipContext;
canvasUpdate: CanvasUpdate; // For replay
};
Interaction Strategies
Each interaction type has a dedicated strategy that:
- Captures the mutation data (what changed)
- Derives implied mutations (relationship changes)
- Creates the canvas update (for undo/redo replay)
Strategy Registry
Strategies are registered and invoked based on interaction type:
const strategyRegistry = {
[InteractionType.ADD_NODE]: addNodeStrategy,
[InteractionType.REMOVE_NODE]: removeNodeStrategy,
[InteractionType.UPDATE_NODE]: updateNodeStrategy,
[InteractionType.ADD_EDGE]: addEdgeStrategy,
[InteractionType.REMOVE_EDGE]: removeEdgeStrategy,
};
Mutation Flow
1. Capture Phase
When a user interacts with the canvas:
- Reducer receives the action
- Strategy is invoked based on interaction type
- Strategy captures mutation data and implied mutations
- Command is pushed to the command stack
- Canvas state is updated
2. Aggregation Phase
When the user clicks Save:
- Commands are grouped by entity ID
- Per-entity mutation timelines are computed
- Optimization rules are applied (e.g., create→delete = no-op)
- Final DTO is built with CREATE, UPDATE, DELETE arrays
// Aggregation produces discriminated DTO
type BuilderDto = {
steps: { create?: [...], update?: [...], delete?: [...] };
questions: { create?: [...], update?: [...], delete?: [...] };
questionAnswers: { create?: [...], update?: [...], delete?: [...] };
answerCategories: { create?: [...], update?: [...], delete?: [...] };
canvas_data: ReactFlowJsonObject;
};
3. Sync Phase
After successful save:
- Backend returns created entity IDs
- Frontend updates node
_existingEntityIdfields - Command stack is reset
- Temporary IDs are replaced with permanent IDs
Relationship Context
Why It Matters
Edges in the canvas represent relationships between entities. When building the DTO, the system must:
- For new edges: Use the
relationshipContextcaptured in the command - For existing edges: Derive relationships from current canvas state
Relationship Types
| Edge Pattern | Relationship | Affected Field |
|---|---|---|
| Question → Answer | Parent reference | answer.question_id |
| Category → Answer | Category assignment | answer.category_id |
| Step → Step | Flow sequence | bookingFlow.steps[] order |
Undo/Redo System
How It Works
- Each command stores
previousDataand acanvasUpdate - Undo moves the pointer backward and reverts canvas state
- Redo moves the pointer forward and re-applies canvas state
Key Behaviors
- Undo reverts canvas state but keeps command in stack
- Redo re-applies canvas state from command
- New action after undo truncates stack (discards redo history)
- Commands past pointer are excluded from DTO aggregation
Canvas Data vs Entity Data
The system maintains strict separation:
| Data Type | Stored In | Persisted Via |
|---|---|---|
| Node positions | canvas_data.nodes[].position | canvas_data field |
| Viewport (zoom, pan) | canvas_data.viewport | canvas_data field |
| Entity content | Entity mutations | CREATE/UPDATE operations |
| Entity relationships | Entity mutations | UPDATE operations |
Position-only movements do NOT create commands. Canvas data is saved as a side effect, separate from the transactional entity operations.
Error Handling
Canvas Persist Failures
Canvas data persistence is fire-and-forget but failures are captured:
type CanvasPersistErrorContext = {
flowId: string;
organizationId: string;
operation: 'create' | 'update';
error: Error;
canvasData: ReactFlowJsonObject;
timestamp: Date;
};
Errors are logged with full context for debugging and potential manual retry.
Idempotent Deletes
All delete operations use idempotent mode for retry resilience:
await service.remove(id, organizationId, {
idempotent: true, // Silently succeeds if already deleted
session,
});
Testing
The mutation tracking system is covered by comprehensive integration tests:
| Test Category | Files | Coverage |
|---|---|---|
| Mutation Tracking | 5 | Node/edge operations, aggregation, relationships |
| Undo/Redo | 5 | Command stack, form sync, complex sequences |
| Validation | 1 | Graph validation rules |
| Full Flow | 1 | End-to-end save and sync |
Total: 23 test files, 502+ tests passing