Saltar a contenido

← Volver al índice

Análisis FSM — Flows como Máquinas de Estados

Tipo: Documentación Técnica — Análisis Profundo
Estado: ✅ Completo
Relacionado con: Decisión Stack Backend


1. La premisa correcta: cada Flow ES una FSM

El usuario tiene razón absoluta: cada flow definido en Pet Flows es, por definición, una Finite State Machine (FSM).

stateDiagram-v2
    [*] --> Trigger: Evento dispara flow

    Trigger --> Wait: Consulta finalizada
    Wait --> Condition: 5 días después
    Condition --> Push: pushEnabled == true
    Condition --> Email: pushEnabled == false
    Push --> Analytics: Push enviado
    Email --> Analytics: Email enviado
    Analytics --> [*]: Flow completado

    note right of Trigger: Estado 1
    note right of Wait: Estado 2 (timer)
    note right of Condition: Estado 3 (decisión)
    note right of Push: Estado 4a (acción)
    note right of Email: Estado 4b (acción)
    note right of Analytics: Estado 5 (final)

Cada nodo = estado. Cada edge = transición. Cada condición = evento que define la transición. Esto es un FSM clásico.


2. Dos frameworks FSM — comparativa real

La pregunta NO es "¿necesitamos FSM?" (sí, claramente). La pregunta es: ¿qué tipo de FSM es más apropiado para este caso de uso?

Pekko FSM (Actor-level FSM)

// Pekko: FSM como comportamiento INTERNO de un Actor
public class FlowExecutionActor extends AbstractFSM<FlowState, FlowData> {
    {
        startWith(IDLE, FlowData.empty());

        when(IDLE, matchEvent(StartFlow.class, (event, data) ->
            goTo(WAITING_TRIGGER).using(data.withFlow(event.getFlow()))
        ));

        when(WAITING_TRIGGER, matchEvent(TriggerFired.class, (event, data) ->
            goTo(EVALUATING_CONDITION).using(data.withTriggerResult(event))
        ));

        when(EVALUATING_CONDITION, matchEvent(ConditionResult.class, (event, data) -> {
            if (event.isTrue()) return goTo(EXECUTING_ACTION_A).using(data);
            else return goTo(EXECUTING_ACTION_B).using(data);
        }));

        when(EXECUTING_ACTION_A, matchEvent(ActionComplete.class, (event, data) ->
            goTo(COMPLETED).using(data.withResult(event))
        ));

        // ... 40 nodos × N estados = explosion combinatoria de states

        onTransition(matchState(null, COMPLETED, () -> {
            // Persist execution result
            saveExecutionStep();
        }));

        initialize();
    }
}

Características: - FSM es el comportamiento interno de UN actor - Estados tipados en compilación → seguridad de tipos - Cada transición es un message handler explícito - El actor ENCAPSULA todo el estado de la ejecución - Supervision tree para fault tolerance - Pekko Persistence para durabilidad (event sourcing)

Problema para flows: Cada flow definition es diferente. 40 tipos de nodos × combinaciones ilimitadas = no puedes pre-definir los estados en compilación. Necesitas un FSM dinámico, no estático.

Dapr Workflows (Workflow-level FSM)

// Dapr: FSM como workflow DINÁMICO que interpreta la definición
public class FlowExecutionWorkflow extends Workflow {
    @Override
    public WorkflowStub create() {
        return ctx -> {
            // Lee la definición del flow (JSON)
            FlowDefinition flow = ctx.getInput(FlowDefinition.class);

            // Recorre los nodos como un FSM dinámico
            FlowNode currentNode = flow.getStartNode();
            Map<String, Object> context = new HashMap<>();

            while (currentNode != null) {
                // Cada nodo = un estado FSM
                // Cada actividad = la acción de ese estado
                ExecutionResult result = ctx.callActivity(
                    currentNode.getType() + "Activity",
                    currentNode.getConfig(),
                    ExecutionResult.class
                ).await();

                // Persistencia automática (event sourcing built-in)
                // Si el proceso se cae aquí, Dapr reanuda desde este punto

                context.putAll(result.getOutputs());

                // Transición FSM: evalúa edges para determinar siguiente estado
                currentNode = evaluateTransition(flow, currentNode, context);
            }
        };
    }

    private FlowNode evaluateTransition(FlowDefinition flow, 
                                         FlowNode current, 
                                         Map<String, Object> ctx) {
        for (FlowEdge edge : flow.getEdgesFrom(current.getId())) {
            if (edge.getCondition() == null || edge.getCondition().evaluate(ctx)) {
                return flow.getNode(edge.getTargetId());
            }
        }
        return null; // FSM terminado
    }
}

Características: - FSM dinámico: interpreta la definición JSON en runtime - Event sourcing y durabilidad automática (Dapr lo gestiona) - Cada callActivity().await() = checkpoint persistente - Si el proceso muere, reanuda desde el último checkpoint - No hay explosion combinatoria — el grafo lo define el usuario - Timers/delays nativos: ctx.createTimer(duration).await()


3. El análisis honesto — ¿cuál modela mejor los flows?

Criterio Pekko FSM Dapr Workflow
¿Es un FSM real? ✅ Sí — puro y tipado ✅ Sí — dinámico
Estados predefinidos 🔴 Requerido en compilación ✅ Dinámicos (JSON)
40 tipos de nodo 🔴 40 message types + handlers ✅ 40 Activity classes
Combinaciones infinitas 🔴 State explosion ✅ Grafo interpretado
Event sourcing ✅ Pekko Persistence (manual) ✅ Built-in (automático)
Durabilidad ante crash ✅ Con Persistence (código extra) ✅ Nativo (0 código)
Timers/delays ✅ FSM.StateTimeout ✅ ctx.createTimer()
Branching (If/Else) ✅ matchEvent + goTo ✅ evaluateTransition()
Paralelismo (fan-out) ✅ Spawn child actors ✅ ctx.allOf()
Sub-flows ✅ Child actors ✅ ctx.callChildWorkflow()
Hot code reload ✅ Erlang-style en Pekko ❌ Requiere redeploy
Monitoring 🟡 Custom (mailbox inspection) ✅ Built-in status API
DX / complejidad 🔴 Alta (actor messages, Props, supervision) ✅ Baja (código imperativo)

4. La distinción clave: FSM estático vs FSM dinámico

PEKKO FSM                           DAPR WORKFLOW
┌──────────────────────┐             ┌──────────────────────┐
│ Flow = Actor          │             │ Flow = Workflow       │
│ Estados = Enum        │             │ Estados = JSON nodes  │
│ Transiciones = Code   │             │ Transiciones = edges  │
│ Compilación: STATIC   │             │ Definición: DYNAMIC   │
│                       │             │                       │
│ Cada flow distinto    │             │ Un workflow ejecutor  │
│ = Actor CLASS distinto│             │ interpreta CUALQUIER  │
│                       │             │ flow definition       │
└──────────────────────┘             └──────────────────────┘

Pekko FSM brilla cuando tienes un conjunto fijo y pequeño de estados conocidos en compilación (ej: OrderStatus = {CREATED, PAID, SHIPPED, DELIVERED}).

Dapr Workflows brilla cuando tienes un FSM dinámico definido por el usuario (ej: el usuario diseña un flow con 3, 5, o 20 nodos en cualquier combinación — no puedes pre-definir esos estados).

[!IMPORTANT] Los flows de Pet Flows son FSMs DINÁMICOS. El usuario diseña el grafo visualmente. No sabemos cuántos estados tendrá, ni qué transiciones. El motor debe interpretar la definición, no compilarla. Esto es exactamente lo que Dapr Workflows hace.


5. ¿Podría Pekko modelar esto? Sí, pero...

Pekko puede ejecutar FSMs dinámicos — necesitarías un "actor intérprete" que camine el grafo:

// Pekko: Actor intérprete de flows (viable pero más complejo)
public class FlowInterpreterActor extends AbstractPersistentActor {
    private FlowDefinition flowDef;
    private String currentNodeId;
    private Map<String, Object> context;

    @Override
    public Receive createReceive() {
        return receiveBuilder()
            .match(StartFlow.class, this::onStart)
            .match(NodeCompleted.class, this::onNodeCompleted)
            .match(TimerExpired.class, this::onTimerExpired)
            .build();
    }

    private void onNodeCompleted(NodeCompleted msg) {
        // Mismo patrón que Dapr — pero manualmente:
        // 1. Persist event
        // 2. Evaluate transitions
        // 3. Execute next node
        // 4. Handle timers
        // 5. Handle errors
        // 6. Handle sub-flows
        // Todo esto Dapr lo hace por ti.
    }

    // + snapshotting manual
    // + supervision strategy
    // + recovery logic
    // + timer management
    // + child actor lifecycle
    // = ~500 líneas más de infraestructura
}

El resultado funcional es idéntico. Pero:

Aspecto Pekko Intérprete Dapr Workflow
Código infraestructura ~500 líneas extra 0 (built-in)
Persistence Manual (event sourcing code) Automático
Recovery Manual (onRecover handler) Automático
Timers Manual (scheduler + actor messages) ctx.createTimer()
Monitoring Manual (custom API) Built-in status API

6. Conclusión

Pregunta Respuesta
¿Los flows son FSMs? Sí, absolutamente
¿Pekko es el mejor framework para FSMs? Para FSMs estáticos, sí
¿Los flows de Pet Flows son estáticos? No — son dinámicos, definidos por el usuario
¿Dapr Workflows modela FSMs dinámicos? Sí — es su propósito exacto
¿Se pierde algo con Dapr vs Pekko? Hot code reload (no crítico para flows)
¿Se gana algo con Dapr vs Pekko? Durabilidad automática, DX, menos código infra

[!TIP] La analogía: Pekko FSM es como escribir tu propio motor de base de datos para gestionar datos estructurados. Funciona, es potente, pero PostgreSQL ya existe y está optimizado para eso. Dapr Workflows ES el "PostgreSQL de los workflow FSMs" — puedes implementarlo tú, pero ¿para qué?


Documentos Relacionados

Nivel Documento
ADR Decisión Stack Backend
Spike Dapr Workflows
Plan Implementation Plan Backend