Já pensou em orquestrar todo o processo de compra de um marketplace com segurança, mesmo se der algum problema no meio do caminho? Hoje vamos fazer exatamente isso usando Azure Durable Functions em Node.js e o padrão de Saga para garantir que, se algo falhar, a gente consiga desfazer as etapas anteriores sem stress. Vamos passo a passo montar uma simulação de compra completa – do pagamento ao envio – num estilo bem descontraído (vem comigo que eu te explico 😉).

Rapidinho: o que é esse tal de Saga?

Antes de botar a mão no código, vale aquela explicação rápida: Saga é um padrão de arquitetura usado para gerenciar transações distribuídas ou processos de negócio de longa duração. Em vez de travar tudo em uma única transação gigante (o que é impraticável num monte de microsserviços), o Saga divide o processo em uma sequência de etapas locais (cada uma com seus próprios commits) e, importante, uma compensação para cada etapa caso algo mais adiante dê errado.

Em bom português: imagine uma compra online com várias etapas (cobrar pagamento, reservar estoque, enviar pedido...). Se lá no final o envio falhar, não adianta ter cobrado o cliente e reservado produto à toa, né? O Saga garante que cada passo desfeito se necessário – tipo um “Ctrl+Z” para cada ação já concluída caso o processo não conclua com sucesso. Assim, mantemos os sistemas consistentes e ninguém fica no prejuízo. Legal, né? 😁

Resumindo o Saga: sequência de passos independentes com rollback individual. Se tudo der certo, maravilha; se algo der ruim no meio, ele desfaz o que já rolou e encerra graciosamente o processo. E aqui vamos implementar isso de forma orquestrada (ou seja, com um orquestrador central coordenando as etapas) usando Azure Durable Functions.

Visão geral do fluxo de compra

Bora ver o fluxo da nossa simulação de compra antes de mergulhar no código. Vamos imaginar uma compra bem simples em um marketplace, envolvendo três etapas principais:

  1. Reservar o estoque do produto comprado (afinal, não podemos vender o que não tem em estoque, então primeiro garantimos a reserva).
  2. Processar o pagamento do cliente (cobrar no cartão, por exemplo).
  3. Agendar o envio do produto (simular o despacho do item para entrega).

E claro, como estamos seguindo o padrão Saga, cada uma das duas primeiras etapas terá uma ação de compensação caso algo depois falhe:

  • Se o pagamento for processado mas o envio falhar, precisamos estornar o pagamento.
  • Se o estoque for reservado mas o pagamento falhar, precisamos liberar o estoque reservado.

No nosso caso, a terceira etapa (envio) é a final; se ela falhar, acionamos as compensações das etapas anteriores e cancelamos a compra. Se todas as etapas passarem, show de bola: compra concluída! ✅

Veja em alto nível:

Fluxograma

  • Sucesso total: Reserva -> Pagamento -> Envio -> fim (compra confirmada).
  • Falha no pagamento: Reserva -> tentativa de Pagamento falha -> estoque liberado -> fim (compra cancelada).
  • Falha no envio: Reserva -> Pagamento -> tentativa de Envio falha -> pagamento estornado + estoque liberado -> fim (compra cancelada).

Com isso em mente, partiu código!

Orquestrando a saga da compra (código do Orchestrator)

No Azure Durable Functions, quem coordena essa brincadeira é o Orchestrator, uma função orquestradora que chama as funções de atividade na ordem certa e decide o que fazer em caso de sucesso ou erro. Vamos construir o nosso orquestrador chamado OrderOrchestrator. Ele vai executar as três etapas na sequência e chamar as funções de compensação se necessário.

No projeto Node.js (usando TypeScript aqui pra dar uma organizada, mas poderia ser JavaScript puro também), o orquestrador é definido como uma função geradora usando a biblioteca durable-functions. Segue o código completo do nosso OrderOrchestrator com comentários explicando cada passo:

import * as df from "durable-functions";

const orchestrator = df.orchestrator(function* (context) {
    const input = context.df.getInput();

    // Passo 1: Reservar estoque
    const inventoryResult = yield context.df.callActivity("ReserveInventory", input.orderDetails);
    if (!inventoryResult.success) {
        // Falhou ao reservar estoque, encerra a saga com erro (nada para compensar aqui, pois nada mais foi feito)
        return { success: false, message: "Compra cancelada: estoque indisponível 😕" };
    }

    // Passo 2: Processar pagamento
    const paymentResult = yield context.df.callActivity("ProcessPayment", input.paymentDetails);
    if (!paymentResult.success) {
        // Falhou pagamento: chama compensação do estoque reservado e termina com erro
        yield context.df.callActivity("ReleaseInventory", input.orderDetails);
        return { success: false, message: "Compra cancelada: pagamento falhou, estoque liberado 💸" };
    }

    // Passo 3: Agendar envio
    const shippingResult = yield context.df.callActivity("ScheduleShipping", { 
        orderId: input.orderDetails.orderId, 
        shippingAddress: input.shippingAddress 
    });
    if (!shippingResult.success) {
        // Falhou envio: estorna pagamento e libera estoque (compensações das etapas anteriores)
        yield context.df.callActivity("RefundPayment", input.paymentDetails);
        yield context.df.callActivity("ReleaseInventory", input.orderDetails);
        return { success: false, message: "Compra cancelada: envio n\u00e3o dispon\u00edvel, pagamento estornado e estoque liberado 🚚" };
    }

    // Se chegou aqui, tudo deu certo!
    return { success: true, message: "Pedido concluido com sucesso! 🥳" };
});

export default orchestrator;

Vamos dissecar o que acontece aí:

  • Entrada (input): O orquestrador espera receber detalhes do pedido, como orderDetails (ex: ID do pedido, itens, etc.), paymentDetails (dados do pagamento) e shippingAddress (endereço para entrega). Daqui a pouco veremos como enviar isso.
  • ReserveInventory: Primeiro chamamos a função de atividade ReserveInventory passando os detalhes do pedido. Se ela retornar { success: false }, quer dizer que não conseguiu reservar (talvez estoque insuficiente). Nesse caso, não adianta prosseguir – a função já devolve um resultado de falha e a saga termina aí mesmo. (Note que nada foi feito antes disso, então não precisamos “desfazer” nada ainda.)
  • ProcessPayment: Se o estoque foi reservado com sucesso, partimos para cobrar o pagamento com ProcessPayment. Se essa etapa retornar falha, aí sim acionamos a compensação do estoque: chamamos ReleaseInventory para liberar aquele estoque que tínhamos reservado, já que a compra não vai mais acontecer. Depois retornamos falha da saga de compra (e nada de tentar envio, porque sem pagamento não rola).
  • ScheduleShipping: Com pagamento ok e estoque reservado, bora enviar o produto! Chamamos ScheduleShipping com os dados necessários (usamos orderId e talvez o endereço de envio). Se por alguma zica essa etapa falhar, precisamos reverter as anteriores: chamamos RefundPayment (estornar o pagamento cobrado) e ReleaseInventory (liberar o estoque). A ordem aqui não importa tanto, mas em geral estornamos pagamento antes de liberar estoque. Depois retornamos a falha.
  • Finalização: Se passou por todas as condições sem retornar antes, significa que reserva, pagamento e envio foram bem-sucedidos. Então retornamos uma mensagem de sucesso geral. 🎉

Repare que o orquestrador coordenou todas as etapas na sequência certinha, e garantiu que, em caso de problema, chamasse imediatamente as funções de compensação necessárias antes de encerrar. Isso é a essência do Saga: cada serviço/atividade é chamado, e se algo depois falha, o orquestrador volta chamando os undos dos passos já feitos.

Agora que o “maestro” Orchestrator está pronto, vamos definir as funções de atividade individuais – tanto as ações principais quanto as de compensação.

Definindo as atividades e compensações da Saga

Temos algumas funções de atividade para criar:

  • ReserveInventory – Reserva os itens no estoque para o pedido.
  • ReleaseInventory – Compensação da reserva de estoque (desfaz a reserva).
  • ProcessPayment – Processa (cobra) o pagamento do cliente.
  • RefundPayment – Compensação do pagamento (faz o reembolso).
  • ScheduleShipping – Agenda o envio do pedido.

Vamos ver cada uma delas com código. Aqui vamos simular as lógicas de forma bem simples, só para focar no fluxo. Em um caso real, essas funções chamariam serviços externos, bancos de dados, APIs de pagamento, etc.

📦 ReserveInventory – reservando o produto no estoque

import { AzureFunction, Context } from "@azure/functions";

const ReserveInventory: AzureFunction = async function (context: Context, orderDetails: any): Promise<{ success: boolean }> {
    context.log(`Reservando estoque para o pedido ${orderDetails.orderId}...`);

    // Simulação de banco de dados de estoque
    const inventory = {
        "item1": 100, // 100 unidades disponíveis
        "item2": 50,  // 50 unidades disponíveis
        "item3": 0,   // 0 unidades disponíveis (sem estoque)
    };

    // Verificando se os itens do pedido estão disponíveis no estoque
    for (let item of orderDetails.items) {
        const availableStock = inventory[item.productId];
        if (availableStock === undefined) {
            context.log(`Item ${item.productId} não encontrado no estoque.`);
            return { success: false };
        }

        if (availableStock < item.quantity) {
            context.log(`Estoque insuficiente para o item ${item.productId}. Disponível: ${availableStock}, Necessário: ${item.quantity}`);
            return { success: false };
        }
    }

    // Simulando a reserva de estoque
    for (let item of orderDetails.items) {
        inventory[item.productId] -= item.quantity;
        context.log(`Reservado ${item.quantity} unidades do item ${item.productId}. Estoque restante: ${inventory[item.productId]}`);
    }

    // Se passou por todas as verificações, consideramos a reserva bem-sucedida
    context.log(`Estoque reservado com sucesso para o pedido ${orderDetails.orderId}`);
    return { success: true };
};

export default ReserveInventory;

Essa função recebe orderDetails (poderia ter ID do produto, quantidade, etc.) e simula a reserva do item. Aqui estamos simplesmente retornando { success: true } para indicar sucesso. Se quiséssemos simular falha de estoque, poderíamos retornar { success: false } dependendo de alguma condição (ex: item fora de estoque).

Dica: Para testar o Saga em caso de falha, você pode alterar temporariamente o retorno para false e ver o orquestrador acionando a compensação 😉.

♻️ ReleaseInventory – liberando o estoque (compensação)

import { AzureFunction, Context } from "@azure/functions";

const ReleaseInventory: AzureFunction = async function (context: Context, orderDetails: any): Promise<void> {
    context.log(`Liberando estoque do pedido ${orderDetails.orderId} (compensando reserva)...`);

    // Simulação de banco de dados de estoque
    const inventory = {
        "item1": 100, // 100 unidades disponíveis
        "item2": 50,  // 50 unidades disponíveis
        "item3": 0,   // 0 unidades disponíveis (sem estoque)
    };

    // Revertendo a reserva de estoque
    for (let item of orderDetails.items) {
        const availableStock = inventory[item.productId];

        // Verifica se o item existe no estoque
        if (availableStock === undefined) {
            context.log(`Item ${item.productId} não encontrado no estoque.`);
            continue; // Se o item não for encontrado, ignoramos
        }

        // Verifica se a quantidade a ser liberada é válida
        if (item.quantity <= 0) {
            context.log(`Quantidade inválida para o item ${item.productId}. Quantidade: ${item.quantity}`);
            continue; // Se a quantidade for inválida, ignoramos
        }

        // Liberando o estoque
        inventory[item.productId] += item.quantity;
        context.log(`Estoque do item ${item.productId} liberado. Quantidade revertida: ${item.quantity}. Estoque atual: ${inventory[item.productId]}`);
    }

    context.log(`Estoque liberado com sucesso para o pedido ${orderDetails.orderId}`);
};

export default ReleaseInventory;

O ReleaseInventory é chamado pelo orquestrador apenas quando precisamos desfazer uma reserva de produto (ou seja, a compra não vai mais ser concluída). Ele recebe o mesmo orderDetails para saber qual pedido/produto liberar e apenas faz um log simulando essa liberação. Numa aplicação real, aqui teríamos a lógica para incrementar o estoque novamente.

💳 ProcessPayment – cobrando o cliente

import { AzureFunction, Context } from "@azure/functions";

const ProcessPayment: AzureFunction = async function (context: Context, paymentDetails: any): Promise<{ success: boolean }> {
    context.log("Processando pagamento do cliente...");

    const { cardNumber, amount } = paymentDetails;

    // Validação de entrada
    if (!cardNumber || cardNumber.length !== 16 || !/^\d+$/.test(cardNumber)) {
        context.log(`Erro: Número de cartão inválido. Card Number: ${cardNumber}`);
        return { success: false };
    }

    if (!amount || amount <= 0) {
        context.log(`Erro: Quantia inválida para o pagamento. Quantia: ${amount}`);
        return { success: false };
    }

    // Aqui seria feita a integração real com o gateway de pagamento (ex: Stripe, PayPal, etc.)
    // Exemplo de integração fictícia com um gateway de pagamento:
    // const paymentGatewayResponse = await callPaymentGateway(paymentDetails);

    // Simulação de sucesso no pagamento
    context.log(`Pagamento de R$ ${amount.toFixed(2)} processado com sucesso com o cartão ${cardNumber.substring(0, 4)}-****-****-${cardNumber.substring(12)}.`);

    // Simulando sempre sucesso no pagamento
    return { success: true };
};

export default ProcessPayment;

O ProcessPayment cuidaria de cobrar o cliente. Recebemos paymentDetails (que pode ter informações do cartão, valor, etc.) e aqui apenas simulamos uma cobrança bem-sucedida retornando { success: true }. Se quiséssemos simular, por exemplo, cartão recusado, retornaríamos { success: false }.

Assim como antes, não se preocupe: deixar esse retorno fixo em true significa que, por padrão, nossa simulação sempre vai passar do pagamento. Mas está fácil de ajustar se quiser ver o fluxo de erro acontecendo.

💸 RefundPayment – estornando o pagamento (compensação)

import { AzureFunction, Context } from "@azure/functions";

const RefundPayment: AzureFunction = async function (context: Context, paymentDetails: any): Promise<void> {
    context.log("Estornando pagamento do pedido (compensando pagamento)...");

    const { cardNumber, amount, orderId } = paymentDetails;

    // Validação de entrada
    if (!cardNumber || cardNumber.length !== 16 || !/^\d+$/.test(cardNumber)) {
        context.log(`Erro: Número de cartão inválido. Card Number: ${cardNumber}`);
        return;
    }

    if (!amount || amount <= 0) {
        context.log(`Erro: Quantia inválida para o estorno. Quantia: ${amount}`);
        return;
    }

    if (!orderId) {
        context.log(`Erro: Identificador do pedido não fornecido.`);
        return;
    }

    // Aqui seria feita a integração real com o sistema de pagamento para processar o estorno
    // Exemplo fictício de integração com um gateway de pagamento:
    // const refundResponse = await callRefundGateway(paymentDetails);

    // Simulação de sucesso no estorno
    context.log(`Pagamento de R$ ${amount.toFixed(2)} estornado com sucesso para o pedido ${orderId}.`);
    context.log(`Estorno realizado com o cartão ${cardNumber.substring(0, 4)}-****-****-${cardNumber.substring(12)}.`);

    // Nenhum valor é retornado, pois é um processo de estorno que não requer resposta
};

export default RefundPayment;

O RefundPayment entra em ação caso a saga precise desfazer uma cobrança já realizada (por exemplo, se o envio do produto falhou depois do pagamento ter sido cobrado). Ele recebe os mesmos paymentDetails e faria a chamada para estornar o valor no cartão do cliente. Na nossa simulação apenas loga uma mensagem. O importante é: essa função será chamada somente se for necessária a compensação do pagamento.

🚚 ScheduleShipping – agendando o envio do pedido

import { AzureFunction, Context } from "@azure/functions";

const ScheduleShipping: AzureFunction = async function (context: Context, shipmentInfo: any): Promise<{ success: boolean }> {
    context.log(`Agendando envio do pedido ${shipmentInfo.orderId}...`);

    // Verificando se as informações necessárias para o agendamento estão presentes
    if (!shipmentInfo.orderId || !shipmentInfo.shippingAddress) {
        context.log("Erro: Informações de envio incompletas.");
        return { success: false };
    }

    // Simulação de integração com API de transportadora
    try {
        // Aqui você chamaria uma API externa de transportadora, por exemplo:
        // const response = await callShippingAPI(shipmentInfo);

        // Simulando a resposta bem-sucedida da transportadora
        const shipmentResponse = {
            trackingNumber: "TRACK123456789",
            estimatedDeliveryDate: "2025-05-10"
        };

        context.log(`Envio agendado com sucesso. Número de rastreamento: ${shipmentResponse.trackingNumber}. Data estimada de entrega: ${shipmentResponse.estimatedDeliveryDate}`);

        return { success: true };
    } catch (error) {
        context.log(`Erro ao agendar o envio: ${error.message}`);
        return { success: false };
    }
};

export default ScheduleShipping;

Por fim, ScheduleShipping simula o agendamento do envio. Recebe algo como shipmentInfo (no nosso caso, contendo o orderId e talvez endereço de entrega), e retorna { success: true } simulando que o pedido foi enviado com sucesso. Se retornássemos false aqui, significaria que não foi possível despachar (imagina um erro na transportadora, por exemplo). Nesse caso, como vimos no orquestrador, o Saga acionaria RefundPayment e ReleaseInventory em seguida.

Vale notar: sendo essa a última etapa, não temos uma função de compensação específica para envio – se o envio falha, a forma de compensar é basicamente cancelar tudo (estornar e liberar estoque). Se o envio dá certo, missão cumprida, não precisamos desfazer nada 🎉.

Agora que todas as funções estão definidas, vamos rodar essa aplicação e ver o Saga funcionando!

Rodando tudo localmente 🚀

Para rodar local, vamos usar o Azure Functions Core Tools. Se você seguiu a estrutura sugerida, já deve ter um projeto de Azure Functions configurado. Caso contrário, aqui um resumão de como criar o projeto e as funções (usando o CLI do Functions):

# (Caso ainda não tenha o projeto configurado)
func init minha-saga-compras --typescript
cd minha-saga-compras

# Cria as funções Durable (Orchestrator e Activities)
func new --template "Durable Functions Orchestration" --name OrderOrchestrator
func new --template "Durable Functions Activity" --name ReserveInventory
func new --template "Durable Functions Activity" --name ProcessPayment
func new --template "Durable Functions Activity" --name ScheduleShipping

# (Crie manualmente ou usando o mesmo template as funções de compensação:)
func new --template "Durable Functions Activity" --name ReleaseInventory
func new --template "Durable Functions Activity" --name RefundPayment

Obs: O Azure Functions Core Tools vai gerar alguns arquivos default para cada função (incluindo um function.json para configuração de triggers e bindings). Nossas funções de atividade usam trigger do tipo Activity, e o orquestrador é do tipo Orchestration. Você não precisa mexer nesses arquivos JSON manualmente, a não ser para ajustar algo específico. O importante é que nosso código acima esteja nos arquivos corretos (OrderOrchestrator/index.ts, ReserveInventory/index.ts, etc., por exemplo).

Antes de rodar, certifique-se de que você tem uma Storage Account configurada para o Azure Functions usar (em local, o Core Tools geralmente usa o Azurite como storage emulado automaticamente). Se ao rodar aparecer erro de storage, instale/rode o Azurite ou configure a connection string de uma storage real no local.settings.json (chave "AzureWebJobsStorage").

Agora, para subir tudo localmente, basta rodar:

func start

Isso vai iniciar seu runtime do Azure Functions em http://localhost:7071. A função orquestradora do Durable Functions por padrão expõe um endpoint HTTP para iniciar a saga. Vamos fazer uma requisição para começar a simulação de compra. Você pode usar o cURL no terminal ou o Postman (ou qualquer ferramenta similar) para enviar uma requisição HTTP POST com um JSON contendo os dados do pedido.

Exemplo usando cURL (no terminal) para iniciar a orquestração da compra:

curl -X POST http://localhost:7071/api/orchestrators/OrderOrchestrator \
  -H "Content-Type: application/json" \
  -d '{
    "orderDetails": { "orderId": "12345", "items": ["item1", "item2"] },
    "paymentDetails": { "cardNumber": "4111111111111111", "amount": 99.90 },
    "shippingAddress": "Rua Exemplo, 123, Cidade XYZ, Brasil"
  }'

Vamos entender esse comando:

  • Estamos fazendo um POST para o endpoint padrão /api/orchestrators/OrderOrchestrator. Esse é um jeito rápido de disparar o orquestrador Durable Functions sem precisar escrever uma função HTTP separada. (Por baixo dos panos, o Durable Functions cuida de criar uma instância do nosso Orchestrator.)
  • No corpo (-d) enviamos um JSON com orderDetails, paymentDetails e shippingAddress. Pode ajustar esses dados como quiser – eles vão aparecer no nosso contexto input do orquestrador.
  • O header Content-Type: application/json é importante para a função receber o body corretamente.

Se estiver usando Postman, é só criar uma nova request POST para http://localhost:7071/api/orchestrators/OrderOrchestrator, selecionar Body como JSON e mandar os mesmos campos.

O que esperar de resposta?

Quando você enviar essa requisição, a Durable Functions vai retornar imediatamente uma resposta HTTP 202 Accepted com alguns dados no header/body indicando que a orquestração foi iniciada (incluindo um instance ID e URLs para consultar o status). Isso porque o processamento em si é assíncrono – acontece no background.

Para simplificar nosso teste, podemos monitorar o log do console onde rodamos o func start. Lá você verá as mensagens que colocamos em cada função:

  • Deve aparecer "Reservando estoque...", depois "Processando pagamento...", depois "Agendando envio...".
  • Se tudo correu bem, a resposta final do orquestrador (success true) deve ser gravada também. Você pode ver o resultado final chamando o status endpoint que a resposta 202 fornece (o URL /api/instances/), mas não vamos complicar: no log já dá pra perceber o fluxo.

Por exemplo, em uma execução bem-sucedida, você deverá ver no log algo como:

Reservando estoque para o pedido 12345...
Processando pagamento do cliente...
Agendando envio do pedido 12345...

E a orquestração finalizando com sucesso. Já em uma execução onde simulamos alguma falha (digamos que fizemos ReserveInventory retornar false), o log mostraria a falha e as compensações:

Reservando estoque para o pedido 12345...
** (falha simulada aqui) **
Liberando estoque do pedido 12345 (compensando reserva)...

E o orquestrador terminaria marcando success: false devido à falha.

Show, né? Você acabou de rodar uma Saga completa localmente!

Publicando no Azure ☁️

Testamos local, tudo certo; agora vamos deployar isso para o Azure pra usar de verdade em ambiente cloud. Os passos principais são:

  1. Criar um Function App no Azure: Você pode fazer isso pelo portal do Azure (criando um recurso do tipo Azure Functions) ou via CLI. Certifique-se de associar uma Storage Account e tal (o portal normalmente faz automaticamente). Anote o nome do Function App (que será parte da URL).

  2. Publicar o código: Com o Azure Functions Core Tools, é simples. Logue na sua conta Azure pelo CLI (az login) se ainda não fez, e então rode no diretório do projeto:

func azure functionapp publish NOME-DO-SEU-FUNCTIONAPP

Onde NOME-DO-SEU-FUNCTIONAPP é o nome que você escolheu no passo 1. Esse comando empacota e envia todas as funções para o Azure. (Alternativamente, você pode usar a extensão do Azure no VS Code e clicar em "Deploy to Function App", tanto faz 👍.)

  1. Configurar app settings (se necessário): No Azure, o Functions precisa da string de conexão de storage (e qualquer outra configuração) nos Application Settings. Normalmente, ao publicar, o local.settings.json não é enviado por segurança. Então, pelo portal do Azure, configure a mesma chave AzureWebJobsStorage com a conexão da sua Storage Account do Azure (se o Function App não a pegou automaticamente). Nesse nosso exemplo, não temos outras configs específicas – os valores padrão bastam.

Com isso, seu Saga Functions está ao vivo! 🌐

Testando a função no Azure (cURL/Postman)

A forma de testar na nuvem é parecida com local, só mudam as URLs e autenticação:

  • No Azure, cada Function App ganha uma URL base: https://.azurewebsites.net.
  • O endpoint para orquestrador será parecido: POST https://.azurewebsites.net/api/orchestrators/OrderOrchestrator.

Por padrão, as funções no Azure exigem uma API key para serem chamadas. Você pode obter uma Function Key no portal do Azure (ou configurar a auth level como anonymous no function.json da função HttpStart se estivesse usando uma). Para facilitar o teste, vamos supor que você definiu a função de orquestração como anônima ou pegou a chave.

No Postman ou cURL, você faria algo assim:

curl -X POST "https://.azurewebsites.net/api/orchestrators/OrderOrchestrator?code=" \
  -H "Content-Type: application/json" \
  -d '{ ... mesmo JSON de antes ... }'

Se tudo estiver configurado certinho, isso retornará 202 Accepted e iniciará a orquestração na nuvem. Você pode então acompanhar o status via Portal do Azure (em Functions > seu function app > Durable Functions > Instances, ou usando a URL de status fornecida na resposta).

Novamente, dá pra verificar os logs (Application Insights ou log streaming) para ver as mensagens das funções rodando em sequência. A saída final de sucesso/falha da saga pode ser conferida consultando o endpoint de status até ver o output contendo nosso { success: true/false, message: "..." }.

E pronto! Sua saga de compra está rodando no Azure, pronta pra receber requisições de compra de verdade (ou melhor, nossas simulações 😜).

Conclusão 🏁

Neste artigo, construímos passo a passo uma simulação de processo de compra usando Azure Durable Functions e implementamos o padrão Saga de forma bem prática. Orquestramos a sequência de reservar estoque → cobrar pagamento → agendar envio, garantindo que se qualquer passo falhar, os anteriores são desfeitos pelas funções de compensação correspondentes. Vimos como rodar tudo localmente e até publicar no Azure, além de testar a função via cURL/Postman.

A grande sacada aqui é perceber como o Azure Durable Functions facilita a vida ao coordenar fluxos complexos: você escreve o “script” do processo no orquestrador (com as devidas compensações) e deixa o runtime gerenciar estado, reexecução em caso de falhas temporárias, etc. O padrão Saga nos assegura consistência no mundo dos microsserviços sem precisar de transações distribuídas malucas.

Agora é com você! 🎮 Que tal brincar com o projeto? Você pode, por exemplo, alterar as funções de atividade para retornar falha em certos cenários e ver o Saga em ação desfazendo tudo. Ou acrescentar novas etapas (quem sabe um envio de e-mail de confirmação no final, ou uma etapa de notificar o vendedor no marketplace) e pensar em quais compensações seriam necessárias para elas.

Fique à vontade para explorar e adaptar este exemplo ao seu caso de uso. O importante é meter a mão na massa e se divertir aprendendo. Espero que tenha curtido essa jornada descontraída pelo mundo das Durable Functions e Sagas. Qualquer dúvida ou ideia, manda aí nos comentários do dev.to! 🚀 Bora codar! Boas compras... digo, bom código! 😉

Referências

Microsoft Learn
Azure Elevate`s blog