Code: Temporal com pausa e compensação

Se o seu fluxo faz mais de uma coisa, ele já está criando estados intermediários no mundo real. Em um dia bom, isso não aparece. Em um dia normal, aparecem timeouts, dependências instáveis e reentregas que fazem o mesmo passo rodar de novo.

O problema não é o retry existir. O problema é quando o retry encosta em efeito colateral: o que era robustez vira duplicidade, e o que era automação vira semanas de correção manual.

// Temporal (TypeScript) — versão mínima: sequência simples
import { proxyActivities } from '@temporalio/workflow';

type Activities = {
  reserveInventory: (orderId: string) => Promise<void>;
  chargePayment: (orderId: string) => Promise<void>;
  createShipment: (orderId: string) => Promise<void>;
};

const { reserveInventory, chargePayment, createShipment } = proxyActivities<Activities>({
  startToCloseTimeout: '2 minutes',
  retry: { maximumAttempts: 3 },
});

export async function OrderWorkflowBasic(orderId: string) {
  await reserveInventory(orderId);
  await chargePayment(orderId);
  await createShipment(orderId);
  return { status: 'COMPLETED' as const };
}

O desafio — executar é fácil; operar sob falha é o que custa caro

A versão mínima funciona enquanto o mundo coopera. Quando não coopera, ela te deixa com o pior tipo de problema: ações parcialmente realizadas e sem um caminho claro de recuo. O time acaba “consertando por fora”, com scripts, exceções e permissões ampliadas, e a operação aprende a normalizar o estado incoerente.

Em produção, o custo real não é a falha em si. É a combinação de falha com falta de pausa, falta de compensação e falta de narrativa simples sobre o que aconteceu.

O cenário — o mesmo fluxo, agora com pausa e compensação explícitas

O ponto de virada é tratar pausa como comportamento legítimo do workflow e compensação como caminho padrão quando a sequência falha no meio. Isso não elimina instabilidade, mas impede que instabilidade vire multiplicador de dano.

// Temporal (TypeScript) — versão operável: pausa + compensação (Saga)
import { proxyActivities, defineSignal, setHandler, condition } from '@temporalio/workflow';

type Activities = {
  reserveInventory: (orderId: string) => Promise<void>;
  releaseInventory: (orderId: string) => Promise<void>;

  chargePayment: (orderId: string) => Promise<void>;
  refundPayment: (orderId: string) => Promise<void>;

  createShipment: (orderId: string) => Promise<void>;
  cancelShipment: (orderId: string) => Promise<void>;

  recordStep: (orderId: string, step: string, meta?: Record<string, unknown>) => Promise<void>;
};

const {
  reserveInventory,
  releaseInventory,
  chargePayment,
  refundPayment,
  createShipment,
  cancelShipment,
  recordStep,
} = proxyActivities<Activities>({
  startToCloseTimeout: '2 minutes',
  retry: { maximumAttempts: 3 },
});

export const pause = defineSignal('pause');
export const resume = defineSignal('resume');

export async function OrderWorkflowOperable(orderId: string) {
  let paused = false;

  setHandler(pause, async () => {
    paused = true;
    await recordStep(orderId, 'PAUSED');
  });

  setHandler(resume, async () => {
    paused = false;
    await recordStep(orderId, 'RESUMED');
  });

  const waitIfPaused = async () => {
    await condition(() => !paused);
  };

  const compensations: Array<() => Promise<void>> = [];

  try {
    await recordStep(orderId, 'START');

    await waitIfPaused();
    await recordStep(orderId, 'RESERVE_INVENTORY');
    await reserveInventory(orderId);
    compensations.push(async () => {
      await recordStep(orderId, 'COMP_RELEASE_INVENTORY');
      await releaseInventory(orderId);
    });

    await waitIfPaused();
    await recordStep(orderId, 'CHARGE_PAYMENT');
    await chargePayment(orderId);
    compensations.push(async () => {
      await recordStep(orderId, 'COMP_REFUND_PAYMENT');
      await refundPayment(orderId);
    });

    await waitIfPaused();
    await recordStep(orderId, 'CREATE_SHIPMENT');
    await createShipment(orderId);
    compensations.push(async () => {
      await recordStep(orderId, 'COMP_CANCEL_SHIPMENT');
      await cancelShipment(orderId);
    });

    await recordStep(orderId, 'DONE');
    return { status: 'COMPLETED' as const };
  } catch (err) {
    await recordStep(orderId, 'FAILED', { error: String(err) });

    for (let i = compensations.length - 1; i >= 0; i--) {
      try {
        await compensations[i]();
      } catch {
        // best effort: o rastro já registra a tentativa
      }
    }

    await recordStep(orderId, 'COMPENSATED');
    return { status: 'COMPENSATED' as const, error: String(err) };
  }
}

Aqui, o workflow deixa de ser uma sequência que “tenta terminar” e vira um processo que aceita a realidade do mundo: ele pode ser pausado quando o risco sobe e pode compensar passos já executados quando algo falha no meio. O registro de etapas dá uma narrativa operacional simples, sem depender de reconstrução manual entre sistemas.

O efeito prático é reduzir estado parcial e reduzir a cauda cara. Em vez de duas equipes discutindo “de quem é o erro”, você tem um caminho de recuo previsível e uma explicação objetiva de onde a sequência parou e o que foi tentado para voltar.

Implicações — pausa e compensação mudam o custo do erro raro

Quando pausa existe, desligar ou “esperar o mundo estabilizar” deixa de ser crise política e vira comportamento normal. Quando compensação existe, o sistema deixa de empurrar para a operação o trabalho de recompor coerência.

Isso muda governança de verdade: a empresa não fica refém do caso médio. Ela consegue operar quando o contexto muda, quando dependências oscilam, e quando o mesmo evento tenta se repetir.

Síntese final — maturidade é conseguir recuar sem improviso

Temporal é valioso quando você trata workflow como produto operacional, não como automação de conveniência. O ganho não é “mais execução”, é a capacidade de pausar, continuar e compensar sem transformar cada falha em um incidente longo.

O fluxo mínimo entrega rapidez. O fluxo operável entrega rapidez com um caminho de volta, que é onde a confiança se preserva.

O que ainda poderia melhorar — sinais de próxima maturidade

O próximo degrau aparece quando idempotência deixa de ser suposição e vira propriedade exigida das activities, quando a pausa tem dono e critérios claros de uso sob impacto, quando compensações cobrem também efeitos externos mais difíceis de desfazer, quando a cadeia de causalidade fica recuperável sem esforço heroico, quando limites de risco disparam interrupções antes da cascata, e quando o recuo deixa de depender de pessoas específicas porque já existe como comportamento do sistema.

Veja também: