Nota para desenvolvedores: Se você prefere ir direto ao código, o repositório completo com a implementação desta arquitetura está disponível aqui: github.com/carol8fml/nexus-notification-hub.
Olá, pessoal! 🤟🏾
Há alguns anos, recebi a tarefa de avaliar serviços externos para envio transacional de e-mails. Na época, já utilizávamos o SendGrid em quatro aplicações web e surgiu a necessidade de expandir essa solução para o aplicativo principal da empresa.
A previsão era um acréscimo de 250.000 envios por mês. A partir disso, analisei mais de 10 provedores e encontrei alternativas equivalentes e mais baratas. No entanto, o verdadeiro custo só apareceu no final: a mão de obra dos desenvolvedores.
Substituir o serviço nas aplicações já integradas ao SendGrid exigiria um esforço enorme, porque a lógica estava fortemente acoplada à implementação do provedor, tanto nos projetos com abordagem funcional quanto naqueles estruturados com princípios próximos da Clean Architecture.
Depois de alinhar com o time, ficou claro que essa mudança levaria meses para entrar nas Sprints, já que seria necessário deslocar desenvolvedores das entregas principais exclusivamente para refatoração.
No fim, continuamos com a ferramenta, mesmo não sendo a opção mais econômica. Na prática, a empresa acabou ficando refém do serviço por uma limitação técnica.
Recentemente, durante meus estudos em Arquitetura de Software, decidi ir além do consumo de teoria e aplicar esse conhecimento para resolver problemas reais que enfrentei ao longo da carreira. Foi dessa experiência que surgiu o Nexus Notification Hub: uma POC criada para validar uma arquitetura orientada ao baixo acoplamento, utilizando uma stack robusta (NestJS, Nx) e padrões clássicos, principalmente o Adapter Pattern.
Se tivéssemos essa estrutura na época, o cenário seria outro:
- Criar apenas um novo arquivo (o Adapter).
- Alterar uma linha de configuração.
- Resolver o problema.
A seguir, mostro como implementei essa solução na prática, permitindo trocar o SendGrid pela AWS ou qualquer outro provedor em minutos, e não meses.
A Arquitetura: Adapter Pattern na Prática
A solução se baseia em dois padrões clássicos trabalhando em conjunto: o Adapter Pattern e o Factory Pattern.
O Adapter foi utilizado para impedir que a regra de negócio conhecesse detalhes de provedores externos. Já o Factory centraliza a escolha do provedor e evita condicionais espalhadas pela aplicação.
A ideia é ter uma camada que impede a lógica de negócio de depender de implementações específicas de serviços de e-mail.
1. O Contrato: A Interface NotificationProvider
Tudo começa com uma interface que define o contrato mínimo que qualquer provedor de notificação precisa seguir:
export interface NotificationProvider {
send(to: string, content: string): Promise<void>;
}
Essa interface define o contrato central da arquitetura. Independentemente de estarmos usando SendGrid, AWS SES, Mailtrap ou qualquer outro serviço, todos precisam implementar o método send com essa assinatura.
Na prática: O restante do código (Controller, Factory, Use Cases) não precisa saber qual serviço está sendo utilizado. Ele trabalha apenas com esse contrato.
2. A Implementação: MailtrapProvider como Adapter
Cada serviço de e-mail possui sua própria forma de integração (API REST, SDK proprietário, SMTP, etc.). O Adapter Pattern resolve isso criando uma tradução entre a interface comum e a implementação específica.
Veja como ficou o adapter do Mailtrap:
@Injectable()
export class MailtrapProvider implements NotificationProvider {
private transporter: nodemailer.Transporter;
constructor(private configService: ConfigService) {
this.transporter = nodemailer.createTransport({
host: this.configService.get<string>('MAILTRAP_HOST'),
port: this.configService.get<number>('MAILTRAP_PORT'),
auth: {
user: this.configService.get<string>('MAILTRAP_USER'),
pass: this.configService.get<string>('MAILTRAP_PASS'),
},
});
}
async send(to: string, content: string): Promise<void> {
await this.transporter.sendMail({
from: 'noreply@nexus.com',
to,
subject: 'Notification from Nexus',
text: content,
html: `<p>${content}</p>`,
});
}
}
Toda a complexidade do nodemailer e da configuração do Mailtrap permanece encapsulada nessa implementação. Se surgir a necessidade de trocar para SendGrid, basta criar um SendGridProvider implementando a mesma interface. O restante do sistema continua funcionando sem alterações.
3. O Factory: Centralizando a Decisão
O Factory Pattern centraliza a lógica de escolha do provedor, evitando if/else espalhados pelo código:
@Injectable()
export class NotificationFactory {
constructor(private mailtrapProvider: MailtrapProvider) {}
getProvider(type: 'email'): NotificationProvider {
switch (type) {
case 'email':
return this.mailtrapProvider;
default:
throw new Error(`Unsupported notification type: ${type}`);
}
}
}
O Factory recebe os providers via Injeção de Dependência do NestJS. Para adicionar um novo provider, basta:
- Criar a classe do Adapter.
- Injetar no Factory.
- Adicionar um
caseno switch.
4. O Controller: Trabalhando com Abstrações
No Controller, o desacoplamento fica evidente. Ele não precisa conhecer Mailtrap, SendGrid ou qualquer outro serviço:
@Controller('api/notifications')
export class NotificationsController {
constructor(private notificationFactory: NotificationFactory) {}
@Post()
async sendNotification(@Body() dto: SendNotificationDto) {
const provider = this.notificationFactory.getProvider(dto.type);
await provider.send(dto.destination, dto.content);
return {
success: true,
message: `Notification sent via ${dto.type}`,
};
}
}
Nota arquitetural: Neste exemplo didático, o cliente define o tipo de notificação via DTO. Em um cenário real, essa decisão poderia ser tomada automaticamente pelo backend (considerando regras de failover, custo ou disponibilidade), mantendo o cliente agnóstico quanto ao provedor.
5. A Cola: Injeção de Dependência (NestJS)
O NestJS gerencia o ciclo de vida e a injeção dessas classes através do módulo:
@Module({
controllers: [NotificationsController],
providers: [NotificationFactory, MailtrapProvider],
})
export class NotificationsModule {}
Vendo na Prática
Abaixo, uma demonstração do fluxo completo: o frontend (React) enviando uma requisição para o backend (NestJS), que utiliza o Adapter do Mailtrap para realizar o envio.
Escalabilidade na Prática
Supondo que seja necessário adicionar o SendGrid, quantos arquivos precisam ser alterados?
-
Criar o Adapter (
sendgrid.provider.ts) implementandoNotificationProvider. - Atualizar o Factory, adicionando o novo provider.
- Atualizar o Módulo, registrando o provider.
Resultado:
O Controller permanece intacto.
A lógica de negócio permanece intacta.
Os testes existentes continuam válidos.
Bônus: DTOs Compartilhados (Nx Monorepo)
Como estamos utilizando Nx, aproveitei para compartilhar os DTOs entre Frontend e Backend, garantindo consistência contratual entre as aplicações.
export class SendNotificationDto {
@IsEnum(['email'])
@IsNotEmpty()
type!: 'email';
@IsString()
@IsNotEmpty()
destination!: string;
// ... outros campos
}
Se o contrato for alterado, o build do Frontend e do Backend falha imediatamente, evitando inconsistências silenciosas de integração.
Conclusão: A Liberdade da Abstração
O Nexus Notification Hub surgiu como uma resposta técnica para um impasse de negócio que vivi no passado. Um problema que antes exigiria meses de refatoração passa a ser resolvido em poucas horas.
Mais do que aplicar padrões ou frameworks, a principal lição foi perceber como decisões arquiteturais influenciam diretamente a capacidade de adaptação de um sistema.
Naquela época, ficamos reféns do SendGrid não porque ele era insubstituível, mas porque o código estava estruturado dessa forma. Com uma arquitetura baseada em abstrações, a decisão volta para as mãos do time e do negócio.
Por outro lado, esse tipo de abordagem também adiciona complexidade e só faz sentido quando existe uma possibilidade real de troca de fornecedor ou crescimento do sistema. Caso contrário, existe o risco de adicionar complexidade sem trazer ganho real para o sistema.
Se hoje surgisse novamente a necessidade de escalar para 250.000 envios mensais trocando de fornecedor, a resposta provavelmente não seria mais "não dá". Seria algo próximo de:
"Ok, me dá uma tarde para implementar o novo Adapter."
Hoje entendo que essa diferença está menos ligada a escrever código e mais a construir soluções que conseguem evoluir com o tempo.
Código Fonte
O projeto completo, incluindo a configuração do Monorepo Nx, o frontend em React e todos os testes, está disponível no GitHub.

Top comments (0)