Un agente de IA no es un endpoint que devuelve una respuesta en 200 ms. Es una secuencia de pasos: llama a un LLM, procesa la respuesta, consulta una base de datos, envía un email, actualiza un registro. Si el proceso falla en el paso 4 de 6, ¿qué haces? ¿Empiezas desde cero? ¿Sabes siquiera en qué paso estabas?
Ese es el problema que resuelven los workflows duraderos. Y en Spring Boot tienes varias formas de atacarlo.
El problema de fondo: ejecución sin memoria de estado
Una petición HTTP normal es stateless. El servidor recibe la petición, la procesa y devuelve la respuesta. Si falla, el cliente reintenta. Simple.
Un agente de IA es diferente. Puede tardar segundos o minutos. Depende de servicios externos que pueden fallar. Y si falla en mitad de la ejecución, volver a empezar desde cero puede ser costoso: el LLM tiene coste por token, los emails no se deben mandar dos veces, y algunas operaciones no son reversibles.
Necesitas dos cosas: saber en qué paso estabas cuando falló, y poder retomar desde ahí sin repetir lo que ya completaste.
Idempotencia: la base de todo
Una operación es idempotente si ejecutarla una vez o diez veces produce el mismo resultado. Es la propiedad que necesitas en cada paso de tu workflow para poder reintentarlo sin miedo.
La forma más directa es usar una clave de idempotencia por cada operación. Antes de ejecutar el paso, comprueba si ya tienes un resultado guardado para esa clave. Si lo tienes, devuelves el resultado guardado sin volver a ejecutar la operación.
@Service
public class IdempotentStepExecutor {
private final StepResultRepository resultRepository;
public <T> T ejecutar(String workflowId, String stepName, Supplier<T> operacion, Class<T> tipo) {
String clave = workflowId + ":" + stepName;
Optional<StepResult> existente = resultRepository.findByClave(clave);
if (existente.isPresent()) {
return deserializar(existente.get().getResultado(), tipo);
}
T resultado = operacion.get();
resultRepository.save(new StepResult(clave, serializar(resultado)));
return resultado;
}
}
Con esto, si el paso de llamar al LLM ya se completó, no vuelves a llamar. Te ahorras coste y tiempo, y el resultado es consistente.
Checkpointing: saber dónde estás
El checkpointing es guardar el estado del workflow tras completar cada paso. No es el resultado del paso, sino el avance del proceso completo.
Una tabla sencilla en base de datos basta para empezar:
CREATE TABLE workflow_state (
id VARCHAR(36) PRIMARY KEY,
tipo VARCHAR(100) NOT NULL,
paso_actual VARCHAR(100),
estado VARCHAR(20), -- PENDING, RUNNING, COMPLETED, FAILED
payload JSON,
created_at TIMESTAMP,
updated_at TIMESTAMP
);
Antes de iniciar cada paso, actualizas paso_actual. Tras completarlo, guardas el resultado en payload y avanzas al siguiente. Si el proceso cae en mitad de un paso, al reiniciar sabes exactamente dónde retomar.
Opciones en Spring Boot
Spring Retry: reintentos simples
Para pasos que pueden fallar por problemas transitorios (timeout de red, servicio externo caído temporalmente), @Retryable es la solución más directa:
@Retryable(
retryFor = {ExternalServiceException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 1000, multiplier = 2)
)
public String llamarLLM(String prompt) {
return llmClient.complete(prompt);
}
@Recover
public String recuperarLLM(ExternalServiceException ex, String prompt) {
log.error("LLM no disponible tras 3 intentos: {}", ex.getMessage());
throw new WorkflowStepFailedException("llm", ex);
}
El backoff exponencial con multiplier = 2 espera 1 segundo en el primer reintento, 2 en el segundo, 4 en el tercero. Evita saturar el servicio que ya está bajo presión. Con Java 25 LTS y Virtual Threads, estos reintentos con espera no bloquean hilos del sistema.
Spring Batch: workflows por lotes
Si tu agente procesa elementos en lote (analiza 1.000 documentos, genera 500 descripciones de producto), Spring Batch es la herramienta adecuada. Gestiona el estado de cada item, reintenta los fallidos y te permite retomar un job desde el último punto de control sin reprocesar lo ya completado.
Spring State Machine: flujos con estados explícitos
Cuando el workflow tiene estados bien definidos y transiciones claras entre ellos, Spring State Machine los modela de forma explícita. Cada estado tiene sus guardas, acciones y transiciones. El estado persiste en base de datos y se puede recuperar en cualquier momento.
Es más verboso que las otras opciones, pero ideal cuando el flujo tiene ramificaciones y las reglas de transición son complejas.
Temporal.io: workflows duraderos complejos
Para workflows de larga duración (minutos, horas o días), Temporal.io es la opción más potente. El SDK de Java te permite escribir el workflow como código secuencial normal, y Temporal gestiona la persistencia, los reintentos y la recuperación de forma transparente:
@WorkflowImpl
public class AgenteContenidoWorkflowImpl implements AgenteContenidoWorkflow {
private final LLMActivity llm = Workflow.newActivityStub(LLMActivity.class,
ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(30)).build());
private final StorageActivity storage = Workflow.newActivityStub(StorageActivity.class,
ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(10)).build());
@Override
public void ejecutar(String tema) {
String contenido = llm.generar(tema); // paso 1
llm.validar(contenido); // paso 2
Long id = storage.guardar(contenido); // paso 3
storage.notificar(id); // paso 4
}
}
Si falla en el paso 3, Temporal retoma desde el paso 3 cuando el worker vuelve a estar disponible. No repite el paso 1 ni el 2.
El patrón saga para transacciones distribuidas
Cuando tu workflow modifica datos en varios servicios y uno de los pasos falla, necesitas deshacer lo que ya hiciste. El patrón saga define una acción compensatoria para cada paso.
Si el paso de enviar el email falla después de haber guardado el documento en la BD, la acción compensatoria podría ser marcar el documento como borrador o eliminarlo. El orden de las compensaciones es inverso al orden de los pasos originales.
Los patrones de diseño como Saga y State Machine en Java son especialmente relevantes en arquitecturas de microservicios donde cada paso puede afectar a un servicio distinto.
Ejemplo completo: agente con recuperación
Juntando todo: un agente que genera contenido con un LLM, lo valida, lo guarda en BD y envía notificación.
@Service
public class AgenteContenidoService {
private final IdempotentStepExecutor stepExecutor;
private final WorkflowStateRepository stateRepo;
private final LLMClient llmClient;
private final ContentRepository contentRepo;
private final NotificationService notificationService;
@Transactional
public void ejecutar(String workflowId, String tema) {
WorkflowState state = stateRepo.findById(workflowId)
.orElseGet(() -> stateRepo.save(new WorkflowState(workflowId, "RUNNING")));
// Paso 1: generar contenido
String contenido = stepExecutor.ejecutar(workflowId, "generar",
() -> llmClient.generar(tema), String.class);
state.setPasoActual("generado");
stateRepo.save(state);
// Paso 2: validar
stepExecutor.ejecutar(workflowId, "validar",
() -> { validar(contenido); return true; }, Boolean.class);
state.setPasoActual("validado");
stateRepo.save(state);
// Paso 3: guardar
Long contentId = stepExecutor.ejecutar(workflowId, "guardar",
() -> contentRepo.save(new Content(tema, contenido)).getId(), Long.class);
state.setPasoActual("guardado");
stateRepo.save(state);
// Paso 4: notificar
stepExecutor.ejecutar(workflowId, "notificar",
() -> { notificationService.enviar(contentId); return true; }, Boolean.class);
state.setEstado("COMPLETED");
stateRepo.save(state);
}
}
Si el proceso se interrumpe en el paso 3, la próxima llamada con el mismo workflowId salta directamente al paso 3 porque los pasos 1 y 2 ya tienen resultado guardado.
API de workflow: empezar, consultar, cancelar
Expón el workflow como un recurso REST con tres operaciones básicas:
POST /workflows: inicia un nuevo workflow y devuelve su ID.GET /workflows/{id}: devuelve el estado actual (paso, estado general, timestamps).DELETE /workflows/{id}: cancela un workflow en curso y ejecuta las compensaciones necesarias.
No bloquees el hilo esperando a que el workflow complete. Inicia la ejecución de forma asíncrona (con @Async o un executor) y devuelve el ID inmediatamente. El cliente puede consultar el estado cuando quiera.
Los workflows duraderos añaden complejidad. Pero cuando un agente de IA cuesta dinero por cada llamada al LLM, o cuando una acción a medias puede dejar datos en estado inconsistente, esa complejidad vale la pena. El punto de partida más simple es la tabla de estado + idempotencia. Solo escala hacia Temporal cuando el caso de uso lo justifique.
Imagen: Pexels / panumas nikhomkhai
