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:
Eventos globais definidos em
packages/contracts:PersonCreated,PersonUpdated(People)InvoiceCreated,PaymentCompleted(Invoices)CommunicationSent(Communications)
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
Payload via DTOs:
- Eventos carregam DTOs imutáveis
- Evita vazamento de modelos Eloquent
- Garante contrato estável
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
ShouldQueuenos 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ãoCreatePerson) - Listeners: Imperativo (
CreateCustomerFromPerson) - DTOs: Sempre imutáveis (
readonlyproperties)
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...
}