<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: José Marcos</title>
    <description>The latest articles on DEV Community by José Marcos (@josemarcosit).</description>
    <link>https://dev.to/josemarcosit</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F255625%2Ff2690f4a-318c-4f75-90be-969fa7408afd.jpeg</url>
      <title>DEV Community: José Marcos</title>
      <link>https://dev.to/josemarcosit</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/josemarcosit"/>
    <language>en</language>
    <item>
      <title>Outbox Pattern na Prática: Mensageria Confiável em um Banco Digital (Portuguese)</title>
      <dc:creator>José Marcos</dc:creator>
      <pubDate>Tue, 04 Nov 2025 02:20:42 +0000</pubDate>
      <link>https://dev.to/josemarcosit/outbox-pattern-na-pratica-mensageria-confiavel-em-um-banco-digital-portuguese-i8d</link>
      <guid>https://dev.to/josemarcosit/outbox-pattern-na-pratica-mensageria-confiavel-em-um-banco-digital-portuguese-i8d</guid>
      <description>&lt;p&gt;Quando falamos de &lt;strong&gt;sistemas financeiros&lt;/strong&gt;, confiabilidade não é um luxo, é um requisito. &lt;br&gt;
Uma transferência bancária não pode simplesmente "sumir" porque o sistema caiu antes de publicar o evento no broker. &lt;/p&gt;

&lt;p&gt;Neste artigo, quero mostrar como resolver esse tipo de problema implementando o &lt;strong&gt;Outbox Pattern&lt;/strong&gt; e &lt;strong&gt;Domain Events&lt;/strong&gt; usando um exemplo prático inspirado em um &lt;strong&gt;banco digital&lt;/strong&gt;. &lt;/p&gt;
&lt;h2&gt;
  
  
  Problema
&lt;/h2&gt;

&lt;p&gt;Imagine o fluxo de uma transferência entre contas: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;O serviço registra a transação no banco de dados. &lt;/li&gt;
&lt;li&gt;Em seguida, publica um evento no broker (RabbitMQ, Kafka, etc.) &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Um fluxo simples de caminho feliz. Até o momento em que o servidor cai ou uma falha de rede acontece. A transação foi salva, mas o evento nunca chegou ao broker.&lt;br&gt;
Resultado: inconsistência entre os sistemas. &lt;/p&gt;
&lt;h2&gt;
  
  
  Solução: Outbox Pattern
&lt;/h2&gt;

&lt;p&gt;A ideia central do Outbox Pattern é armazenar os eventos junto com a transação do banco de dados, garantindo atomicidade. &lt;br&gt;
Ou seja, ou tudo é persistido (entidade + evento), ou nada é. &lt;/p&gt;

&lt;p&gt;Um processo em background lê periodicamente esses eventos na tabela OutboxMessages e os publica com segurança, marcando-os como processados.&lt;/p&gt;

&lt;p&gt;Dessa forma, mesmo que o sistema caia, o evento continua guardado e será reenviado depois. &lt;/p&gt;
&lt;h2&gt;
  
  
  Cenário: Banco Digital
&lt;/h2&gt;

&lt;p&gt;Nesta POC, o contexto é o de um banco digital com operações de transferência entre contas. &lt;/p&gt;

&lt;p&gt;A estrutura da solução é a seguinte:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BancoDigital
├── Application
│   ├── Interfaces
│   └── UseCases
├── Domain
│   ├── Entidades
│   ├── Eventos
│   └── Abstracoes
├── Infrastructure
│   ├── DbContext (EF Core)
│   ├── Interceptors (Outbox)
│   ├── Outbox (OutboxDispatcher)
│   └── Serializers
└── BancoDigital.Api
    ├── DTOs
    └── Program.cs

&amp;gt; Aqui optei por adotar uma convenção de nomenclatura de abordagem mista inspirada nos princípios do Domain-Driven Design (DDD).
As camadas arquiteturais seguem a convenção tradicional em inglês (domain, application, infrastructure e interfaces) para manter compatibilidade com a literatura e frameworks amplamente utilizados.
Entretanto, todos os elementos do domínio (entidades, objetos de valor, serviços, eventos, casos de uso etc.) utilizam nomes em português, refletindo fielmente a linguagem ubíqua do negócio.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Entidades Principais
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; public class Conta : ITemEventoDomain
 {
     private readonly List&amp;lt;EventoDomain&amp;gt; _eventosDomain = new();
     public IReadOnlyCollection&amp;lt;EventoDomain&amp;gt; EventosDomain =&amp;gt; _eventosDomain.AsReadOnly();

     public Guid Id { get; private set; }
     public decimal Saldo { get; private set; }

     public Conta(Guid id, decimal saldoInicial = 0m)
     {
         Id = id;
         Saldo = saldoInicial;
     }

     public void Creditar(decimal valor)
     {
         if (valor &amp;lt;= 0) throw new ArgumentException("Valor deve ser positivo", nameof(valor));
         Saldo += valor;
     }

     public void Debitar(decimal valor)
     {
         if (valor &amp;lt;= 0) throw new ArgumentException("Valor deve ser positivo", nameof(valor));
         if (Saldo &amp;lt; valor) throw new InvalidOperationException("Saldo insuficiente");
         Saldo -= valor;
     }

     public void TransferirPara(Conta contaDestino, decimal valor)
     {
         Debitar(valor);
         contaDestino.Creditar(valor);

         _eventosDomain.Add(new TransferenciaRealizadaDomainEvent(
             ContaOrigemId: this.Id,
             ContaDestinoId: contaDestino.Id,
             Valor: valor
         ));
     }

     public void LimparEventosDomain() =&amp;gt; _eventosDomain.Clear();
 }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A entidade de &lt;strong&gt;Conta&lt;/strong&gt; dispara um &lt;strong&gt;EventoDomain&lt;/strong&gt; automaticamente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; public void TransferirPara(Conta contaDestino, decimal valor)
 {
     Debitar(valor);
     contaDestino.Creditar(valor);

     _eventosDomain.Add(new TransferenciaRealizadaDomainEvent(
         ContaOrigemId: this.Id,
         ContaDestinoId: contaDestino.Id,
         Valor: valor
     ));
 }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Tabela Outbox
&lt;/h2&gt;

&lt;p&gt;Cada evento de domínio é serializado e armazenado em uma tabela auxiliar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; public class OutboxMessage
 {
     public long Id { get; set; }
     public DateTime OccurredOnUtc { get; set; }
     public string Type { get; set; } = null!;
     public string Payload { get; set; } = null!;
     public DateTime? ProcessedOnUtc { get; set; }
     public string? Error { get; set; }
 }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Um interceptor do &lt;strong&gt;EF Core&lt;/strong&gt; detecta os eventos e os grava nessa tabela dentro da mesma transação do banco.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public override async ValueTask&amp;lt;InterceptionResult&amp;lt;int&amp;gt;&amp;gt; SavingChangesAsync(
    DbContextEventData eventData,
    InterceptionResult&amp;lt;int&amp;gt; result,
    CancellationToken cancellationToken = default)
{
    var context = eventData.Context;
    if (context == null) return result;

    var aggregates = context.ChangeTracker
        .Entries()
        .Where(e =&amp;gt; e.Entity is IHasDomainEvents hasEvents &amp;amp;&amp;amp; hasEvents.DomainEvents.Any())
        .Select(e =&amp;gt; (IHasDomainEvents)e.Entity)
        .ToList();

    if (!aggregates.Any()) return result;

    var outbox = context.Set&amp;lt;OutboxMessage&amp;gt;();

    foreach (var aggregate in aggregates)
    {
        foreach (var @event in aggregate.DomainEvents)
        {
            outbox.Add(new OutboxMessage
            {
                OccurredOnUtc = @event.OccurredOnUtc,
                Type = @event.GetType().AssemblyQualifiedName!,
                Payload = SystemTextJsonEventSerializer.Serialize(@event)
            });
            _logger.LogInformation("Outbox message {Type} stored ", @event.GetType().Name);
        }
        aggregate.ClearDomainEvents();
    }

    return result;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Processamento Assíncrono
&lt;/h2&gt;

&lt;p&gt;O serviço &lt;strong&gt;OutboxDispatcher&lt;/strong&gt; é executado em background. Ele busca eventos não processados e os "envia" para um broker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; protected override async Task ExecuteAsync(CancellationToken stoppingToken)
 {
     while (!stoppingToken.IsCancellationRequested)
     {
         using var scope = _sp.CreateScope();
         var db = scope.ServiceProvider.GetRequiredService&amp;lt;BancoDigitalDbContext&amp;gt;();

         var pendentes = await db.OutboxMessages
             .Where(o =&amp;gt; o.ProcessedOnUtc == null)
             .OrderBy(o =&amp;gt; o.Id)
             .Take(50)
             .ToListAsync(stoppingToken);

         foreach (var msg in pendentes)
         {
             try
             {
                 var evt = SystemTextJsonEventSerializer.Deserialize(msg.Payload, msg.Type) as DomainEvent;
                 if (evt != null)
                 {                     
                     await SimulatedPublish(evt);  
                     msg.ProcessedOnUtc = DateTime.UtcNow;
                 }
             }
             catch (Exception ex)
             {
                 msg.Error = ex.Message;                 
             }
         }

         if (pendentes.Any())
         {
             await db.SaveChangesAsync(stoppingToken);
         }

         await Task.Delay(_pollInterval, stoppingToken);
     }
 } 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Resultados
&lt;/h2&gt;

&lt;p&gt;Com essa abordagem, a consistência é garantida: &lt;/p&gt;

&lt;p&gt;O evento nunca se perde. &lt;br&gt;
Falhas momentâneas no broker não impactam a transação. &lt;br&gt;
O sistema continua eventual e previsivelmente consistente. &lt;/p&gt;

&lt;p&gt;Exemplo de logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[INF] Outbox message TransferenciaRealizadaDomainEvent armazenada 
[INF] Evento TransferenciaRealizada publicado: d2ae0477-6d9b-44a2-bc10-849b6d11fef0 -&amp;gt; 8d425533-c790-4ec8-8bcb-79537dd318d1 : Valor 10 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Próximos Passos
&lt;/h2&gt;

&lt;p&gt;Integrar o Outbox com RabbitMQ ou Kafka. &lt;br&gt;
Implementar retry policies com Polly. &lt;br&gt;
Adicionar métricas e monitoramento da Outbox. &lt;/p&gt;

&lt;h2&gt;
  
  
  Referências
&lt;/h2&gt;

&lt;p&gt;Artigo original: &lt;a href="https://dev.to/stevsharp/reliable-messaging-in-net-domain-events-and-the-outbox-pattern-with-ef-core-interceptors-pjp"&gt;Reliable Messaging in .NET: Domain Events and the Outbox Pattern with EF Core Interceptors &lt;/a&gt;&lt;br&gt;
Microsoft Docs – &lt;a href="https://learn.microsoft.com/pt-br/ef/core/logging-events-diagnostics/interceptors" rel="noopener noreferrer"&gt;Interceptors in EF Core &lt;/a&gt;&lt;br&gt;
Microsoft Docs – &lt;a href="https://learn.microsoft.com/pt-br/dotnet/core/extensions/workers" rel="noopener noreferrer"&gt;Worker services in .NET &lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusão
&lt;/h2&gt;

&lt;p&gt;O &lt;strong&gt;Outbox Pattern&lt;/strong&gt; é um exemplo elegante de como &lt;strong&gt;simples decisões de arquitetura&lt;/strong&gt; evitam grandes dores de cabeça em produção. &lt;/p&gt;

&lt;p&gt;Em um sistema bancário, onde cada evento importa, ele se torna uma camada essencial de &lt;strong&gt;segurança transacional&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;O código completo da POC está disponível no repositório:&lt;br&gt;
&lt;a href="https://github.com/josemarcosit/bancodigital-api" rel="noopener noreferrer"&gt;https://github.com/josemarcosit/bancodigital-api&lt;/a&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>designpatterns</category>
      <category>microservices</category>
    </item>
    <item>
      <title>3 Tools for testing your kafka connection</title>
      <dc:creator>José Marcos</dc:creator>
      <pubDate>Fri, 23 Oct 2020 18:59:25 +0000</pubDate>
      <link>https://dev.to/josemarcosit/3-tools-for-testing-your-kafka-connection-3ii7</link>
      <guid>https://dev.to/josemarcosit/3-tools-for-testing-your-kafka-connection-3ii7</guid>
      <description>&lt;p&gt;Today, I needed to test whether my kafka broker was active. I used telnet for this, but I could also have used cUrl. These are 3 tools that you can use to test your kafka connection:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool 1 - telnet&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Telnet is a tool that we use to make remote connections based on the telnet protocol.&lt;/p&gt;

&lt;p&gt;The command is quite simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;telnet [host/ip] [port]&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Run the following command in your terminal:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;telnet kafka-01 9092&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your terminal should look like this:&lt;/p&gt;

&lt;p&gt;$ telnet kafka-01 9092&lt;br&gt;
Trying 192.168.15.20...&lt;br&gt;
Connected to kafka-01.&lt;br&gt;
Escape character is '^]'.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool 2 - cUrl&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;cURL is a command-line tool that we use for transferring data from or to a server using various network protocols. I have been using this tool for testing connectivity from a pod running inside a Kubernetes cluster.&lt;/p&gt;

&lt;p&gt;The commad syntax is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;curl [options...] url&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;--&lt;br&gt;
$ curl -v telnet://kafka-01:9092&lt;br&gt;
   Trying 192.168.15.20:9092...&lt;br&gt;
   TCP_NODELAY set&lt;br&gt;
   Connected to kafka-01 (192.168.15.20) port 9092 (#0&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool 3 - kafkacat&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;kafkacat is a powerful tool that you can use to produce, consume, list topics, and partitions. This tool is capable of creating or destroying your entire cluster. Be careful with this tool!&lt;/p&gt;

&lt;p&gt;Run the following command in your terminal:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;kafkacat -L -b kafka-01:9092&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The arguments are -L for listing mode and -b for which Kafka broker to talk to.&lt;/p&gt;

&lt;p&gt;Your terminal should look like this:&lt;/p&gt;

&lt;p&gt;$ kafkacat -L -b kafka-01:9092&lt;br&gt;
Metadata for all topics (from broker -1: kafka-01:9092/bootstrap):&lt;br&gt;
 1 brokers:&lt;br&gt;
  broker 0 at kafka-01:9092 (controller)&lt;br&gt;
 1 topics:&lt;br&gt;
  topic "topic-one" with 1 partition:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;plus:&lt;/strong&gt; You can also use these tools for testing your Azure Event Hubs connections.&lt;/p&gt;

&lt;p&gt;Azure Event Hubs is a distributed stream processing platform and event ingestion service managed by Microsoft. You do not need to set up, configure, and manage your own Kafka clusters.&lt;/p&gt;

&lt;p&gt;This is a terminal output for telnet:&lt;/p&gt;

&lt;p&gt;$ telnet eventhub-01.servicebus.windows.net 9093&lt;br&gt;
Trying [ip]...&lt;br&gt;
Connected to [hostname].cloudapp.net.&lt;br&gt;
Escape character is '^]'.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
