Skip to main content

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:

  1. Captures the mutation data (what changed)
  2. Derives implied mutations (relationship changes)
  3. 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:

  1. Reducer receives the action
  2. Strategy is invoked based on interaction type
  3. Strategy captures mutation data and implied mutations
  4. Command is pushed to the command stack
  5. Canvas state is updated

2. Aggregation Phase

When the user clicks Save:

  1. Commands are grouped by entity ID
  2. Per-entity mutation timelines are computed
  3. Optimization rules are applied (e.g., create→delete = no-op)
  4. 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:

  1. Backend returns created entity IDs
  2. Frontend updates node _existingEntityId fields
  3. Command stack is reset
  4. 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 relationshipContext captured in the command
  • For existing edges: Derive relationships from current canvas state

Relationship Types

Edge PatternRelationshipAffected Field
Question → AnswerParent referenceanswer.question_id
Category → AnswerCategory assignmentanswer.category_id
Step → StepFlow sequencebookingFlow.steps[] order

Undo/Redo System

How It Works

  1. Each command stores previousData and a canvasUpdate
  2. Undo moves the pointer backward and reverts canvas state
  3. 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 TypeStored InPersisted Via
Node positionscanvas_data.nodes[].positioncanvas_data field
Viewport (zoom, pan)canvas_data.viewportcanvas_data field
Entity contentEntity mutationsCREATE/UPDATE operations
Entity relationshipsEntity mutationsUPDATE operations
Important

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 CategoryFilesCoverage
Mutation Tracking5Node/edge operations, aggregation, relationships
Undo/Redo5Command stack, form sync, complex sequences
Validation1Graph validation rules
Full Flow1End-to-end save and sync

Total: 23 test files, 502+ tests passing