Spraxium logoSpraxium

Padrões de Arquitetura de Listeners

Modele listeners como fronteiras finas de evento que delegam para serviços injetados. Organize módulos e estrutura de arquivos para manter o tratamento de eventos sustentável à medida que o bot cresce.

Listeners como fronteiras de evento

Um listener deve ser a fronteira mais fina possível entre um evento do Discord e a sua lógica de domínio. A função dele é receber o payload do evento, fazer filtros básicos, como ignorar contas de bot, e então chamar o serviço responsável pela regra de negócio. Quando o listener fica fino, o código de domínio continua testável sem precisar simular o sistema de eventos do Discord, e o próprio listener segue legível porque faz muito pouco por conta própria.

O caminho oposto, que é jogar a lógica de negócio diretamente dentro dos métodos do listener, até funciona em bots pequenos, mas degrada rápido quando a base cresce. Se o algoritmo de ranking da sua leaderboard vive dentro de MessageCreateListener, você não consegue testá-lo sem simular mensagens do Discord. Se amanhã a mesma lógica precisar rodar em uma rota de API ou em um job agendado, você vai ter de extrair isso de qualquer forma. Começar com delegação para serviço evita esse retrabalho desde o início.

Esse padrão também deixa bem claro onde investigar quando algo quebra. Se o evento não dispara de jeito nenhum, o problema está no binding do listener ou nos gateway intents. Se o evento dispara, mas o resultado é errado, o problema tende a estar no serviço. Como cada camada tem uma responsabilidade estreita e objetiva, o debug fica mais rápido.

Exemplo de delegação para serviço

Aqui está o padrão em prática. O listener recebe MessageCreate, verifica se o autor é um bot e depois delega para um LeaderboardService, que cuida da lógica real de tracking. O listener não sabe como a pontuação funciona, e o serviço não precisa saber que foi chamado a partir de um evento do Discord.

import { Events, Listener, On } from '@spraxium/common';
import { Logger } from '@spraxium/core';
import type { Message } from 'discord.js';
import { LeaderboardService } from '../../services/leaderboard.service';

@Listener(Events.MessageCreate)
export class MessageCreateListener {
  private readonly logger = new Logger(MessageCreateListener.name);

  constructor(private readonly leaderboard: LeaderboardService) {}

  @On()
  async onMessage(message: Message): Promise<void> {
    if (message.author.bot) return;
    this.logger.debug(`Message from ${message.author.id} in guild ${message.guildId}`);
    await this.leaderboard.trackMessage(message.author.id, message.guildId);
  }
}

O serviço é uma classe injetável comum, registrada pelo módulo no array providers. O listener a recebe por injeção de construtor e chama seus métodos. Se amanhã a mesma regra de pontuação precisar ser usada em outro ponto de entrada, basta injetar o mesmo serviço ali, sem duplicar lógica.

Organização do módulo

Um módulo de eventos bem organizado agrupa listeners relacionados e mantém o arquivo de declaração do módulo na raiz da pasta. Conforme o bot passa a lidar com mais eventos, essa estrutura continua escalando sem virar um labirinto. Cada arquivo de listener cuida de um evento, e o arquivo do módulo funciona como índice do que está realmente ativo.

Módulo de eventos com serviços
ready.listener.ts
message-create.listener.ts
member-join.listener.ts
member-leave.listener.ts
leaderboard.service.ts
welcome.service.ts
events.module.ts

O módulo amarra tudo listando listeners e providers em um único lugar. Quem abre esse arquivo entende imediatamente quais eventos aquela área trata e quais serviços estão disponíveis para injeção.

src/events/events.module.ts
import { Module } from '@spraxium/common';
import { LeaderboardService } from '../services/leaderboard.service';
import { WelcomeService } from '../services/welcome.service';
import { MemberJoinListener } from './listeners/member-join.listener';
import { MemberLeaveListener } from './listeners/member-leave.listener';
import { MessageCreateListener } from './listeners/message-create.listener';
import { ReadyListener } from './listeners/ready.listener';

@Module({
  providers: [LeaderboardService, WelcomeService],
  listeners: [
    ReadyListener,
    MessageCreateListener,
    MemberJoinListener,
    MemberLeaveListener,
  ],
})
export class EventsModule {}

Padrões práticos para métodos de listener

Métodos de listener devem seguir uma estrutura consistente para continuar previsíveis em toda a codebase. Comece com retornos antecipados para eventos irrelevantes, faça qualquer transformação leve necessária e só depois chame o serviço. Esse padrão em três passos deixa cada método fácil de revisar e difícil de complicar sem necessidade.

Filtre eventos irrelevantes primeiro

Comece com guard clauses que retornam cedo para eventos que você não quer tratar. Verificar se o autor é um bot, se a mensagem veio de um canal específico ou se a interação tem o tipo correto deve acontecer antes de qualquer trabalho real. Isso evita chamadas desnecessárias ao serviço e ajuda a manter o throughput de eventos alto.

Extraia só o que o serviço precisa

Puxe do payload apenas os dados que o serviço realmente espera. Em vez de empurrar o objeto inteiro do Discord.js para o serviço, extraia user ID, guild ID, channel ID, ou qualquer outro campo específico necessário. Isso desacopla sua camada de serviço dos tipos do Discord e facilita testes com mocks simples.

Delegue para o serviço injetado

Chame o método do serviço com os dados já extraídos. Nesse ponto, o papel do listener termina. Se o serviço lançar um erro, o pipeline de exceções cuida disso nos eventos suportados, então você não precisa encher o método do listener com try-catch por padrão.

Evite construtor pesado

Não execute trabalho caro no construtor do listener, como abrir conexão com banco, fazer request HTTP ou ler arquivos. O construtor roda durante o bootstrap, antes do bot se conectar ao Discord. Se houver lógica de inicialização, mova isso para a primeira chamada do handler ou para um hook de ciclo de vida, caso o framework ofereça esse suporte.