Skip to content

ADR-003: Event-Driven Communication

Status: Aceito

Data: 2024-11-02

Decisores: Aron Cardoso

Contexto

Com arquitetura monorepo, módulos (packages/invoices, packages/people, packages/communications) precisam comunicar-se sem criar dependências diretas entre si, mantendo o desacoplamento mesmo estando no mesmo repositório.

Forças em jogo:

  • Desacoplamento: Módulos devem evoluir independentemente
  • Escalabilidade: Sistema deve suportar novos módulos sem refatoração
  • Rastreabilidade: Operações importantes devem ser auditáveis
  • Reatividade: Módulos devem reagir a mudanças em outros módulos

Alternativas Consideradas

Opção 1: Chamadas Diretas (HTTP/RPC)

  • Prós:
    • Resposta síncrona e imediata
    • Controle direto de fluxo
    • Mais familiar para desenvolvedores
  • Contras:
    • Acoplamento temporal (módulo chamador espera resposta)
    • Acoplamento estrutural (conhece endpoint do módulo chamado)
    • Dificuldade de escalar horizontalmente
    • Falha em cascata (se um módulo cai, outros falham)

Opção 2: Message Queue (RabbitMQ/SQS)

  • Prós:
    • Desacoplamento total
    • Retry automático
    • Escalabilidade horizontal
    • Garantia de entrega
  • Contras:
    • Infraestrutura adicional (broker externo)
    • Complexidade operacional
    • Custo de manutenção
    • Overkill para projeto inicial

Opção 3: Event-Driven via Laravel Events (Escolhida)

  • Prós:
    • Desacoplamento via contratos
    • Nativo do Laravel (sem deps externas)
    • Auditável via listeners
    • Permite evolução para queue externa
    • Simplicidade inicial
  • Contras:
    • Síncrono por padrão (mas pode usar queue)
    • Menos robusto que message broker dedicado
    • Debugging pode ser mais difícil

Decisão

Adotamos Event-Driven Architecture usando Laravel Events com as seguintes características:

  1. Eventos globais definidos em packages/contracts:

    • PersonCreated, PersonUpdated (People)
    • InvoiceCreated, PaymentCompleted (Invoices)
    • CommunicationSent (Communications)
  2. Listeners em módulos consumidores:

    • Cada módulo registra listeners para eventos relevantes
    • Listeners são isolados (falha de um não afeta outros)
    • Podem ser movidos para queue facilmente
  3. Payload via DTOs:

    • Eventos carregam DTOs imutáveis
    • Evita vazamento de modelos Eloquent
    • Garante contrato estável
  4. Caminho de evolução:

    • Início: eventos síncronos
    • Crescimento: mover para Laravel Queue
    • Escala: migrar para RabbitMQ/SQS mantendo mesmo contrato

Consequências

Positivas

  • Desacoplamento total: Módulos não conhecem uns aos outros
  • Facilita testes: Listeners podem ser mockados facilmente
  • Rastreabilidade: Logs de eventos formam audit trail
  • Extensibilidade: Novos módulos apenas adicionam listeners
  • Zero infraestrutura extra: Usa recursos nativos do Laravel
  • Permite async: Basta implementar ShouldQueue nos listeners

Negativas

  • Debugging complexo: Fluxo não é linear (precisa rastrear eventos)
  • Eventual consistency: Pode haver delay entre evento e reação
  • Ordem não garantida: Múltiplos listeners executam em ordem indefinida
  • Possível duplicação: Retry pode causar processamento duplicado (precisa idempotência)

Neutras

  • Necessita disciplina: Contratos de eventos devem ser versionados
  • Monitoramento essencial: Precisa observabilidade de eventos

Notas de Implementação

Estrutura de Eventos

php
// packages/contracts/src/Events/PersonCreated.php
namespace FilamentCore\Contracts\Events;

use FilamentCore\Contracts\DTOs\PersonDTO;

class PersonCreated
{
    public function __construct(
        public readonly PersonDTO $person,
        public readonly \DateTimeImmutable $occurredAt
    ) {}
}

Registro de Listeners

php
// packages/invoices/src/Providers/EventServiceProvider.php
protected $listen = [
    PersonCreated::class => [
        CreateCustomerFromPerson::class,
    ],
];

Exemplo de Listener

php
namespace FilamentCore\Invoices\Listeners;

use FilamentCore\Contracts\Events\PersonCreated;

class CreateCustomerFromPerson
{
    public function handle(PersonCreated $event): void
    {
        // Extrair DTO
        $personDTO = $event->person;
        
        // Criar customer no módulo Invoices
        Customer::create([
            'external_id' => $personDTO->id,
            'name' => $personDTO->name,
            'email' => $personDTO->contact->primaryEmail,
        ]);
    }
}

Auditoria de Eventos

php
// Listener global para audit trail
class LogAllEvents
{
    public function handle(object $event): void
    {
        if ($event instanceof ContractEvent) {
            EventLog::create([
                'type' => get_class($event),
                'payload' => json_encode($event),
                'occurred_at' => now(),
            ]);
        }
    }
}

Padrões de Naming

  • Eventos: Past tense (PersonCreated, não CreatePerson)
  • Listeners: Imperativo (CreateCustomerFromPerson)
  • DTOs: Sempre imutáveis (readonly properties)

Idempotência

Listeners devem ser idempotentes (podem ser executados múltiplas vezes sem efeito colateral):

php
public function handle(PersonCreated $event): void
{
    // Check if already processed
    if (Customer::where('external_id', $event->person->id)->exists()) {
        return;
    }
    
    // Process event...
}

Referências

Documentação privada do ecossistema Filament Core.