Saga Pattern Orchestration: o maestro que nunca falha nas transações distribuídas
Imagine que você está processando um pedido de R$ 3.500,00 no seu e-commerce e o pagamento foi aprovado, mas o serviço de confirmação do pedido caiu. Resultado sem Orchestration Saga: cliente foi cobrado, mas o pedido não existe no sistema. Resultado COM Orchestration Saga: sistema automaticamente detecta a falha, estorna o pagamento e notifica o cliente - tudo sem intervenção manual.
Este é o poder do Saga Pattern Orchestration- um maestro inteligente que coordena transações distribuídas e nunca deixa seu sistema em estado inconsistente.

Demonstração rápida: veja o maestro funcionando
Antes de mergulhar na teoria, vamos ver um Orchestration Saga resolvendo o problema real:
public class OrderProcessingSaga : SagaBase
{
protected override void InitializeSteps()
{
Steps.Add(new ReserveInventoryStep()); // Passo 1: Reservar estoque
Steps.Add(new ProcessPaymentStep()); // Passo 2: Processar pagamento
Steps.Add(new ConfirmOrderStep()); // Passo 3: Confirmar pedido
Steps.Add(new NotifyCustomerStep()); // Passo 4: Notificar cliente
}
}
[HttpPost]
public async Task<IActionResult> CreateOrder(CreateOrderRequest request)
{
var saga = new OrderProcessingSaga();
var result = await saga.StartAsync(request);
if (result.IsSuccess)
{
return Ok(new { OrderId = result.OrderId });
}
else
{
return BadRequest(new { Error = result.ErrorMessage });
// Sistema já fez rollback automático de tudo!
}
}
O que acontece por baixo dos panos:
- Orchestrator central coordena todos os steps sequencialmente
- Se qualquer step falhar, todos os anteriores são automaticamente compensados
- Se step 3 falhar → system estorna pagamento (step 2) e libera estoque (step 1)
- Estado é persistido a cada step para garantir recuperação
- Zero código de rollback manual necessário
O que é Saga Pattern Orchestration?
Saga Orchestration é um padrão onde um coordenador central(o Orchestrator) gerencia toda a sequência de operações distribuídas, garantindo que se algo falhar, tudo seja compensado automaticamente.
A analogia do maestro de orquestra
Imagine uma orquestra sinfônica tocando uma peça complexa:
MAESTRO (Orchestrator)
O que o maestro faz quando algo dá errado:
- Para a música imediatamente
- Instrui cada seção a voltar ao estado inicial
- Recomeça do início ou cancela a apresentação
No Saga Orchestration é exatamente igual:
- Para a saga imediatamente quando step falha
- Executa compensação de todos os steps já executados
- Marca saga como falhada com estado consistente
Por que Orchestration vs outras abordagens?
Problema sem coordenação: Código frágil e perigoso
public async Task ProcessOrder(OrderRequest request)
{
await _inventoryService.ReserveStock(request.Items); // Service A
await _paymentService.ProcessPayment(request.Amount); // Service B
await _orderService.ConfirmOrder(request.OrderId); // Service C
// Se Service C falhar → A e B ficam inconsistentes!
// Precisa de código manual para desfazer A e B
}
Solução com Orchestration: Orquestração inteligente e segura
public class OrderProcessingSaga : SagaBase
{
protected override void InitializeSteps()
{
Steps.Add(new ReserveInventoryStep()); // Compensation: ReleaseInventory
Steps.Add(new ProcessPaymentStep()); // Compensation: RefundPayment
Steps.Add(new ConfirmOrderStep()); // Compensation: CancelOrder
}
// Orchestrator automaticamente executa compensação na ordem reversa
// Se qualquer step falhar → rollback automático e estado consistente
}
Implementação
Vamos construir um Saga Orchestration:
1. Base class do Orchestrator
public abstract class SagaBase
{
public Guid SagaId { get; } = Guid.NewGuid();
public EnumSagaStatus Status { get; private set; } = EnumSagaStatus.Running;
public List<ISagaStep> Steps { get; } = new();
public List<SagaStepHistory> StepHistory { get; } = new();
private readonly ISagaRepository _sagaRepository;
private readonly ISagaMetrics _sagaMetrics;
private readonly ILogger<SagaBase> _logger;
protected abstract void InitializeSteps();
public async Task<SagaResult> StartAsync(CancellationToken cancellationToken = default)
{
var stopwatch = Stopwatch.StartNew();
try
{
_sagaMetrics.RecordSagaStarted(GetType().Name);
_logger.LogInformation("Starting saga {SagaId} of type {SagaType}", SagaId, GetType().Name);
InitializeSteps();
// Persistir estado inicial
await _sagaRepository.SaveAsync(GetSagaState());
foreach (var step in Steps)
{
var stepResult = await ExecuteStepWithRetryAsync(step, cancellationToken);
if (!stepResult.IsSuccess)
{
_logger.LogWarning("Step {StepName} failed for saga {SagaId}: {Error}",
step.StepName, SagaId, stepResult.ErrorMessage);
await CompensateAsync();
return SagaResult.Failed(stepResult.ErrorMessage);
}
// Atualizar estado após cada step
RecordStepSuccess(step, stepResult);
await _sagaRepository.UpdateAsync(GetSagaState());
}
Status = EnumSagaStatus.Completed;
await _sagaRepository.UpdateAsync(GetSagaState());
stopwatch.Stop();
_sagaMetrics.RecordSagaCompleted(GetType().Name, Status, stopwatch.Elapsed);
_logger.LogInformation("Saga {SagaId} completed successfully in {Duration}ms",
SagaId, stopwatch.ElapsedMilliseconds);
return SagaResult.Success();
}
catch (Exception ex)
{
_logger.LogError(ex, "Saga {SagaId} failed with exception", SagaId);
await CompensateAsync();
stopwatch.Stop();
_sagaMetrics.RecordSagaCompleted(GetType().Name, EnumSagaStatus.Failed, stopwatch.Elapsed);
return SagaResult.Failed(ex.Message);
}
}
private async Task<StepResult> ExecuteStepWithRetryAsync(ISagaStep step, CancellationToken cancellationToken)
{
var retryPolicy = Policy
.Handle<Exception>(ex => IsRetryableException(ex))
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
onRetry: (outcome, timespan, retryCount, context) =>
{
_logger.LogWarning("Retrying step {StepName} (attempt {RetryCount}) after {Delay}ms",
step.StepName, retryCount, timespan.TotalMilliseconds);
});
return await retryPolicy.ExecuteAsync(async () =>
{
using var timeoutCts = new CancellationTokenSource(step.Timeout);
using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken, timeoutCts.Token);
var command = BuildCommandForStep(step);
return await step.ExecuteAsync(command, combinedCts.Token);
});
}
private async Task CompensateAsync()
{
Status = EnumSagaStatus.CompensationRequired;
await _sagaRepository.UpdateAsync(GetSagaState());
_logger.LogWarning("Starting compensation for saga {SagaId}", SagaId);
// Compensar na ordem REVERSA (último executado primeiro)
var executedSteps = StepHistory
.Where(h => h.Status == EnumSagaStatus.Completed)
.OrderByDescending(h => h.ExecutedAt)
.ToList();
var compensationFailures = new List<string>();
foreach (var stepHistory in executedSteps)
{
try
{
var step = Steps.First(s => s.StepName == stepHistory.StepName);
var command = GetCommandForStep(step);
_logger.LogInformation("Compensating step {StepName} for saga {SagaId}",
step.StepName, SagaId);
await step.CompensateAsync(command);
RecordStepCompensated(step);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to compensate step {StepName} for saga {SagaId}",
stepHistory.StepName, SagaId);
compensationFailures.Add($"{stepHistory.StepName}: {ex.Message}");
RecordStepCompensationFailed(stepHistory.StepName, ex.Message);
// Alertar operações para intervenção manual
await _alertingService.SendCriticalAlertAsync(
$"Compensation failed for step {stepHistory.StepName} in saga {SagaId}");
}
}
if (compensationFailures.Any())
{
Status = EnumSagaStatus.CompensationFailed;
await _sagaRepository.UpdateAsync(GetSagaState());
throw new CompensationException($"Failed to compensate: {string.Join(", ", compensationFailures)}");
}
else
{
Status = EnumSagaStatus.Compensated;
await _sagaRepository.UpdateAsync(GetSagaState());
_logger.LogInformation("Successfully compensated saga {SagaId}", SagaId);
}
}
}
public enum EnumSagaStatus
{
Running,
Completed,
Failed,
CompensationRequired,
Compensated,
CompensationFailed
}
2. Interface e base para Steps
public interface ISagaStep
{
string StepName { get; }
TimeSpan Timeout { get; }
Task<StepResult> ExecuteAsync(object command, CancellationToken cancellationToken = default);
Task CompensateAsync(object command, CancellationToken cancellationToken = default);
}
public abstract class SagaStepBase<TCommand, TResponse> : ISagaStep
{
protected readonly ILogger<SagaStepBase<TCommand, TResponse>> _logger;
public abstract string StepName { get; }
public abstract TimeSpan Timeout { get; }
public abstract Task<TResponse> ExecuteAsync(TCommand command, CancellationToken cancellationToken = default);
public abstract Task CompensateAsync(TCommand command, CancellationToken cancellationToken = default);
public async Task<StepResult> ExecuteAsync(object command, CancellationToken cancellationToken = default)
{
if (command is not TCommand typedCommand)
{
throw new ArgumentException($"Expected {typeof(TCommand).Name} but got {command.GetType().Name}");
}
try
{
var result = await ExecuteAsync(typedCommand, cancellationToken);
return StepResult.Success(result);
}
catch (Exception ex) when (IsBusinessException(ex))
{
return StepResult.Failed(ex.Message, isRetryable: false);
}
catch (Exception ex)
{
return StepResult.Failed(ex.Message, isRetryable: true);
}
}
public async Task CompensateAsync(object command, CancellationToken cancellationToken = default)
{
if (command is not TCommand typedCommand)
{
throw new ArgumentException($"Expected {typeof(TCommand).Name} but got {command.GetType().Name}");
}
await CompensateAsync(typedCommand, cancellationToken);
}
protected virtual bool IsBusinessException(Exception ex)
{
return ex is BusinessLogicException || ex is ValidationException;
}
}
public class StepResult
{
public bool IsSuccess { get; private set; }
public string ErrorMessage { get; private set; } = string.Empty;
public bool IsRetryable { get; private set; }
public object? Data { get; private set; }
public static StepResult Success(object? data = null) => new() { IsSuccess = true, Data = data };
public static StepResult Failed(string error, bool isRetryable = true) => new()
{
IsSuccess = false,
ErrorMessage = error,
IsRetryable = isRetryable
};
}
3. Step concreto: Reservar estoque
public class ReserveInventoryStep : SagaStepBase<ReserveInventoryCommand, InventoryReservedResponse>
{
private readonly IInventoryService _inventoryService;
public override string StepName => "ReserveInventory";
public override TimeSpan Timeout => TimeSpan.FromSeconds(30);
public override async Task<InventoryReservedResponse> ExecuteAsync(
ReserveInventoryCommand command,
CancellationToken cancellationToken = default)
{
_logger.LogInformation("Reserving inventory for order {OrderId} with {ItemCount} items",
command.OrderId, command.Items.Count);
var reservationIds = new List<Guid>();
foreach (var item in command.Items)
{
// Verificar disponibilidade primeiro (operação idempotente)
var availability = await _inventoryService.CheckAvailabilityAsync(
item.ProductId, item.Quantity, cancellationToken);
if (!availability.IsAvailable)
{
_logger.LogWarning("Insufficient stock for product {ProductId}. Available: {Available}, Requested: {Requested}",
item.ProductId, availability.AvailableQuantity, item.Quantity);
throw new InsufficientStockException(
$"Product {item.ProductId} has only {availability.AvailableQuantity} units available, but {item.Quantity} were requested");
}
// Reservar estoque (operação atômica)
var reservation = await _inventoryService.ReserveAsync(
item.ProductId, item.Quantity, command.OrderId, cancellationToken);
reservationIds.Add(reservation.Id);
_logger.LogInformation("Reserved {Quantity} units of product {ProductId} with reservation {ReservationId}",
item.Quantity, item.ProductId, reservation.Id);
}
var response = new InventoryReservedResponse
{
OrderId = command.OrderId,
ReservationIds = reservationIds,
ExpiresAt = DateTime.UtcNow.AddMinutes(15), // TTL para limpeza automática
TotalItemsReserved = command.Items.Sum(i => i.Quantity)
};
_logger.LogInformation("Successfully reserved inventory for order {OrderId}. Reservations: {ReservationIds}",
command.OrderId, string.Join(", ", reservationIds));
return response;
}
public override async Task CompensateAsync(
ReserveInventoryCommand command,
CancellationToken cancellationToken = default)
{
_logger.LogInformation("Compensating inventory reservation for order {OrderId}", command.OrderId);
try
{
// Buscar todas as reservas feitas para este pedido
var reservations = await _inventoryService.GetReservationsByOrderAsync(
command.OrderId, cancellationToken);
if (!reservations.Any())
{
_logger.LogInformation("No reservations found for order {OrderId}. Compensation not needed.",
command.OrderId);
return;
}
foreach (var reservation in reservations)
{
// Operação idempotente - se já foi liberada, não faz nada
await _inventoryService.ReleaseReservationAsync(reservation.Id, cancellationToken);
_logger.LogInformation("Released reservation {ReservationId} for product {ProductId} (quantity: {Quantity})",
reservation.Id, reservation.ProductId, reservation.Quantity);
}
_logger.LogInformation("Successfully compensated inventory reservation for order {OrderId}. Released {Count} reservations.",
command.OrderId, reservations.Count());
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to compensate inventory reservation for order {OrderId}. Manual intervention required!",
command.OrderId);
// Escalar para operações
await _alertingService.SendCriticalAlertAsync(
$"Failed to release inventory reservation for order {command.OrderId}. Manual cleanup required!");
throw; // Re-throw para marcar compensação como falhada
}
}
}
Command e Response models
public record ReserveInventoryCommand
{
public Guid OrderId { get; init; }
public List<OrderItem> Items { get; init; } = new();
public DateTime RequestedAt { get; init; } = DateTime.UtcNow;
}
public record InventoryReservedResponse
{
public Guid OrderId { get; init; }
public List<Guid> ReservationIds { get; init; } = new();
public DateTime ExpiresAt { get; init; }
public int TotalItemsReserved { get; init; }
}
public record OrderItem
{
public Guid ProductId { get; init; }
public int Quantity { get; init; }
public decimal UnitPrice { get; init; }
}
4. Step de pagamento com retry inteligente
public class ProcessPaymentStep : SagaStepBase<ProcessPaymentCommand, PaymentProcessedResponse>
{
private readonly IPaymentGateway _paymentGateway;
public override string StepName => "ProcessPayment";
public override TimeSpan Timeout => TimeSpan.FromMinutes(2); // Payments podem demorar
public override async Task<PaymentProcessedResponse> ExecuteAsync(
ProcessPaymentCommand command,
CancellationToken cancellationToken = default)
{
_logger.LogInformation("Processing payment for order {OrderId} amount {Amount} {Currency}",
command.OrderId, command.Amount, command.Currency);
// Verificar se pagamento já foi processado (idempotência)
var existingTransaction = await _paymentGateway.GetTransactionByOrderIdAsync(
command.OrderId, cancellationToken);
if (existingTransaction != null)
{
_logger.LogInformation("Payment already processed for order {OrderId}. Transaction: {TransactionId}",
command.OrderId, existingTransaction.TransactionId);
return new PaymentProcessedResponse
{
OrderId = command.OrderId,
TransactionId = existingTransaction.TransactionId,
Amount = existingTransaction.Amount,
Currency = existingTransaction.Currency,
ProcessedAt = existingTransaction.ProcessedAt,
Status = existingTransaction.Status
};
}
// Processar pagamento com retry para falhas transitórias
var paymentRequest = new PaymentRequest
{
OrderId = command.OrderId,
Amount = command.Amount,
Currency = command.Currency,
PaymentMethod = command.PaymentMethod,
CardToken = command.CardToken,
IdempotencyKey = $"order-{command.OrderId}-{DateTime.UtcNow:yyyyMMdd}" // Chave de idempotência
};
var paymentResult = await _paymentGateway.ProcessPaymentAsync(paymentRequest, cancellationToken);
switch (paymentResult.Status)
{
case PaymentStatus.Approved:
_logger.LogInformation("Payment approved for order {OrderId}. TransactionId: {TransactionId}",
command.OrderId, paymentResult.TransactionId);
return new PaymentProcessedResponse
{
OrderId = command.OrderId,
TransactionId = paymentResult.TransactionId,
Amount = paymentResult.Amount,
Currency = paymentResult.Currency,
ProcessedAt = paymentResult.ProcessedAt,
Status = PaymentStatus.Approved
};
case PaymentStatus.Declined:
_logger.LogWarning("Payment declined for order {OrderId}. Reason: {Reason}",
command.OrderId, paymentResult.DeclineReason);
throw new PaymentDeclinedException(paymentResult.DeclineReason ?? "Payment was declined");
case PaymentStatus.Pending:
_logger.LogInformation("Payment is pending for order {OrderId}. Will retry...", command.OrderId);
throw new PaymentPendingException("Payment is still being processed");
default:
throw new PaymentProcessingException($"Unexpected payment status: {paymentResult.Status}");
}
}
public override async Task CompensateAsync(
ProcessPaymentCommand command,
CancellationToken cancellationToken = default)
{
_logger.LogInformation("Compensating payment for order {OrderId}", command.OrderId);
try
{
// Buscar transação original
var transaction = await _paymentGateway.GetTransactionByOrderIdAsync(
command.OrderId, cancellationToken);
if (transaction == null)
{
_logger.LogInformation("No payment transaction found for order {OrderId}. Compensation not needed.",
command.OrderId);
return;
}
if (transaction.Status != PaymentStatus.Approved)
{
_logger.LogInformation("Payment for order {OrderId} was not approved (status: {Status}). No refund needed.",
command.OrderId, transaction.Status);
return;
}
// Verificar se refund já foi processado
var existingRefund = await _paymentGateway.GetRefundByTransactionIdAsync(
transaction.TransactionId, cancellationToken);
if (existingRefund != null)
{
_logger.LogInformation("Refund already processed for transaction {TransactionId}. RefundId: {RefundId}",
transaction.TransactionId, existingRefund.RefundId);
return;
}
// Processar refund
var refundRequest = new RefundRequest
{
OriginalTransactionId = transaction.TransactionId,
Amount = transaction.Amount,
Reason = "Order cancelled due to saga failure",
IdempotencyKey = $"refund-{transaction.TransactionId}-{DateTime.UtcNow:yyyyMMdd}"
};
var refundResult = await _paymentGateway.RefundAsync(refundRequest, cancellationToken);
if (refundResult.Status == RefundStatus.Approved)
{
_logger.LogInformation("Successfully refunded payment for order {OrderId}. RefundId: {RefundId}",
command.OrderId, refundResult.RefundId);
}
else
{
_logger.LogError("Failed to refund payment for order {OrderId}. Status: {Status}, Reason: {Reason}",
command.OrderId, refundResult.Status, refundResult.DeclineReason);
throw new RefundFailedException(
$"Refund failed with status {refundResult.Status}: {refundResult.DeclineReason}");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to compensate payment for order {OrderId}. Manual refund required!",
command.OrderId);
// Escalar para operações financeiras
await _alertingService.SendCriticalAlertAsync(
$"Failed to refund payment for order {command.OrderId}. Manual refund required! Exception: {ex.Message}");
throw; // Critical: payment compensation must succeed
}
}
}
Vantagens do Orchestration Saga
Benefícios principais:
1. Controle centralizado
- Debugging simplificado: Todo fluxo visível em um lugar
- Lógica de negócio clara: Sequência de steps bem definida
- Monitoramento fácil: Estado centralizado para observar
2. Tratamento de falhas robusto
- Compensação automática: Rollback na ordem correta
- Retry inteligente: Diferencia falhas transitórias vs permanentes
- Estado persistido: Recuperação após restart da aplicação
3. Testabilidade
- Steps isolados: Cada step pode ser testado independentemente
- Mock fácil: Substituir dependencies dos steps em testes
- Cenários de falha: Simular falhas em qualquer step
4. Evolução controlada
- Versionamento: Adicionar/remover steps sem quebrar sagas em andamento
- A/B testing: Diferentes implementações de steps
- Rollback seguro: Voltar versão anterior sem perder sagas ativas
Armadilhas do Orchestration e como evitá-las
Problemas comuns:
1. Orchestrator como bottleneck
PERIGOSO- todos os pedidos passam pelo mesmo orchestrator
Vai virar gargalo com alto volume!
public class SingletonOrderOrchestrator
{
private static readonly OrderOrchestrator _instance = new();
}
Solução:
SEGURO- instância por saga Nova instância para cada pedido
public class OrdersController : ControllerBase
{
[HttpPost]
public async Task<IActionResult> CreateOrder(CreateOrderRequest request)
{
var saga = new OrderProcessingSaga(_serviceProvider);
var result = await saga.StartAsync(request);
return result.IsSuccess ? Ok(result) : BadRequest(result);
}
}
2. Steps muito grandes e acoplados
PERIGOSO- step faz muita coisa
public class ProcessOrderStep : SagaStepBase<ProcessOrderCommand, ProcessOrderResponse>
{
public override async Task<ProcessOrderResponse> ExecuteAsync(ProcessOrderCommand command)
{
// Fazendo tudo em um step
await ValidateCustomer(command.CustomerId);
await ReserveInventory(command.Items);
await ProcessPayment(command.Amount);
await GenerateInvoice(command.OrderId);
await SendEmail(command.CustomerEmail);
// Se qualquer coisa falhar, perde todo o trabalho!
}
}
Solução:
// SEGURO - steps granulares
public class OrderProcessingSaga : SagaBase
{
protected override void InitializeSteps()
{
Steps.Add(new ValidateCustomerStep()); // Step 1: rápido e simples
Steps.Add(new ReserveInventoryStep()); // Step 2: pode falhar por estoque
Steps.Add(new ProcessPaymentStep()); // Step 3: pode falhar por cartão
Steps.Add(new GenerateInvoiceStep()); // Step 4: raramente falha
Steps.Add(new SendEmailStep()); // Step 5: não crítico
}
}
3. Não implementar timeouts
PERIGOSO- pode travar para sempre Se gateway travar, saga trava para sempre
public override async Task<PaymentResponse> ExecuteAsync(PaymentCommand command)
{
return await _paymentGateway.ProcessAsync(command);
}
Solução:
SEGURO- timeout em todos os steps
public override TimeSpan Timeout => TimeSpan.FromMinutes(2);
public override async Task<PaymentResponse> ExecuteAsync(
PaymentCommand command,
CancellationToken cancellationToken)
{
using var timeoutCts = new CancellationTokenSource(Timeout);
using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken, timeoutCts.Token);
try
{
return await _paymentGateway.ProcessAsync(command, combinedCts.Token);
}
catch (OperationCanceledException) when (timeoutCts.Token.IsCancellationRequested)
{
throw new StepTimeoutException($"Payment step timed out after {Timeout}");
}
}
Quando usar Orchestration Saga
Use Orchestration quando:
1. Processo de negócio complexo e bem definido
- E-commerce order processing
- Loan approval workflows
- Insurance claims processing
- Multi-step onboarding processes
2. Debugging e auditoria são críticos
- Sistemas financeiros
- Healthcare applications
- Compliance-heavy industries
3. Controle fino sobre o fluxo é necessário
- Conditional branching baseado em resultados
- Parallel execution de alguns steps
- Dynamic step selection baseado em contexto
4. Equipe centralizada ou pequena
- Time pequeno que pode manter orchestrator
- Ownership centralizado do processo de negócio
NÃO use quando:
1. Performance extrema é necessária
- Orchestrator adiciona latência de network
- Steps sequenciais podem ser lentos
2. Serviços completamente autônomos
- Teams independentes querem controle total
- Microservices com bounded contexts bem definidos
3. Escalabilidade horizontal extrema
- Milhões de sagas simultâneas
- Orchestrator pode virar bottleneck (gargalo)
Resumo: o maestro que nunca falha
Saga Orchestration é o padrão ideal quando você precisa de controle centralizado sobre transações distribuídas complexas.
Próximo passo:
No próximo artigo, vamos explorar Saga Choreography- a abordagem descentralizada onde serviços se coordenam através de eventos, oferecendo melhor performance e escalabilidade, mas com complexidade adicional de debugging.
Próximos passos:
- Implemente os exemplos em um projeto teste
- Configure monitoramento desde o primeiro dia
- Comece com casos de uso simples antes de processos complexos
Recursos adicionais:
- Documentação oficial do .NET sobre transações distribuídas
- Enterprise Integration Patterns - Process Manager
- Microservices Patterns by Chris Richardson
Gostou do conteúdo? Conecte-se comigo no LinkedIn para mais artigos sobre arquitetura de software e desenvolvimento .NET!