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 |