Clean Architecture Backend
O que é Clean Architecture?
Imagine construir uma casa onde você pode trocar a cozinha sem afetar o quarto, ou mudar o sistema elétrico sem mexer no encanamento. Clean Architecture faz isso com código: separa o que importa (regras de negócio) do que pode mudar (tecnologia).
Problema que Resolve
Arquitetura ruim:
Regras de Negócio ← misturado com → Banco de Dados
↕ ↕
API Gateway Framework
Se mudar banco, quebra tudo! ❌
Clean Architecture:
Regras de Negócio (centro, protegido)
↑
Adapters conectam com tecnologia (AWS, Prisma, etc)
Pode trocar tecnologia sem afetar regras! ✅
Filosofia em Uma Frase
"Lambda, Fastify, Prisma e SQS são detalhes que podem ser trocados sem afetar o que realmente importa: as regras de negócio."
Como Funciona na Prática?
Estrutura: Um Shopping com Andares Bem Definidos
🏢 PRÉDIO BACKEND BITIO
4º Andar - INTERFACES │ 🚪 "Portarias de Entrada"
│ Lambda, Worker, Cron Jobs
───────────────────────────────┼─────────────────────────────
3º Andar - APPLICATION │ 🎯 "Gerência de Casos de Uso"
│ Orquestra regras de negócio
───────────────────────────────┼─────────────────────────────
2º Andar - DOMAIN │ 💎 "Cofre das Regras"
│ Entidades, regras, validações
───────────────────────────────┼─────────────────────────────
1º Andar - INFRASTRUCTURE │ 🔧 "Subsolo Técnico"
│ Prisma, SQS, S3, APIs externas
Regra de Ouro
Dependências sempre apontam para CIMA - camadas inferiores nunca conhecem camadas superiores:
- ✅ Application pode usar Domain
- ✅ Infrastructure implementa interfaces do Domain
- ❌ Domain NUNCA importa Infrastructure
- ❌ Domain NUNCA conhece AWS, HTTP, etc
Organização por Domínio de Negócio
Cada módulo representa uma área de negócio específica do restaurante, como se fossem departamentos independentes de uma empresa:
src/modules/
├── cardapio/ # 🍽️ Departamento de Cardápio
│ ├── domain/ # ├─ Regras sobre produtos, categorias
│ ├── application/ # ├─ Casos de uso (criar produto, etc)
│ └── infrastructure/ # └─ Como salvar no banco
├── clientes/ # 👥 Departamento de Clientes
├── reservas/ # 📅 Departamento de Reservas
├── fidelidade/ # 💎 Programa de Fidelidade
├── campanhas/ # 📱 Marketing e Comunicação
├── avaliacoes/ # ⭐ Reputação e Reviews
└── publicidade/ # 📊 Anúncios e Mídia Paga
Regra de Isolamento entre Departamentos
❌ ERRADO: Departamento de Fidelidade acessando tabela de Campanhas
❌ ERRADO: Módulo de Reservas importando repositório de Clientes
✅ CORRETO: Comunicação via casos de uso públicos
✅ CORRETO: Comunicação via eventos assíncronos
✅ CORRETO: Comunicação via IDs (nunca objetos completos)
Exemplo Prático
// ❌ ERRADO - módulo fidelidade acessando diretamente
async function calcularPontos(clienteId: string) {
// Não faça isso!
const campanhas = await campanhasRepository.buscarAtivas();
}
// ✅ CORRETO - comunicação via caso de uso
async function calcularPontos(clienteId: string) {
const campanhasAtivas = await buscarCampanhasAtivasUseCase.execute();
}
// ✅ CORRETO - comunicação via evento
async function onReservaConfirmada(evento: ReservaConfirmadaEvent) {
await adicionarPontosFidelidadeUseCase.execute({
clienteId: evento.clienteId,
pontos: 100
});
}
As 4 Camadas Explicadas
1º Andar: Infrastructure (O Subsolo Técnico)
Função: Implementar conexões com tecnologias externas
É como o subsolo de um prédio - onde ficam os sistemas técnicos:
- Gerador (banco de dados)
- Bombas d'água (APIs externas)
- Sistema elétrico (SQS, S3)
- Elevadores (conexões entre sistemas)
// Como salvamos uma reserva no banco (Prisma)
export class PrismaReservaRepository implements ReservaRepository {
constructor(private readonly prisma: PrismaClient) {}
async salvar(reserva: Reserva): Promise<void> {
// ✅ Converte objeto do domínio para formato do banco
const dadosPrisma = ReservaMapper.paraPrisma(reserva);
await this.prisma.reserva.create({
data: {
...dadosPrisma,
// ✅ SEMPRE filtrar por restaurante (multi-tenant)
restauranteId: reserva.restauranteId,
}
});
}
}
// Como publicamos uma mensagem (SQS)
export class SQSPublisherMessage implements PublicadorMensagem {
async publicar(topico: string, evento: EventoDominio): Promise<void> {
await this.sqsClient.sendMessage({
QueueUrl: this.obterUrlFila(topico),
MessageBody: JSON.stringify(evento),
});
}
}
2º Andar: Domain (O Cofre das Regras)
Função: Guardar as regras de negócio mais importantes
É como o cofre do banco - onde ficam as coisas mais valiosas e protegidas:
- Regras que nunca mudam (validações de negócio)
- Entidades principais (Cliente, Reserva, Produto)
- Validações críticas (não pode reservar no passado)
// Entidade Reserva com suas regras
export class Reserva {
private constructor(
private readonly _id: ReservaId,
private readonly _restauranteId: RestauranteId,
private readonly _dadosCliente: DadosCliente,
private readonly _dataHora: DataHora,
private readonly _numeroPessoas: NumeroPessoas,
private _status: StatusReserva
) {}
static criar(dados: CriarReservaInput): Reserva {
// ✅ Regras de negócio sempre validadas no domínio
if (dados.dataHora.ePassado()) {
throw new DataReservaInvalidaError("Não é possível reservar no passado");
}
if (dados.numeroPessoas > 12) {
throw new MuitasPessoasError("Máximo 12 pessoas por reserva");
}
return new Reserva(
ReservaId.gerar(),
dados.restauranteId,
dados.dadosCliente,
dados.dataHora,
dados.numeroPessoas,
StatusReserva.PENDENTE
);
}
confirmar(): void {
// ✅ Regra de negócio: só pode confirmar se estiver pendente
if (this._status !== StatusReserva.PENDENTE) {
throw new ReservaJaProcessadaError();
}
this._status = StatusReserva.CONFIRMADA;
}
}
3º Andar: Application (A Gerência)
Função: Orquestrar casos de uso e coordenar o trabalho
É como a sala da diretoria - onde as decisões são tomadas e o trabalho é coordenado:
- Recebe solicitações
- Coordena diferentes departamentos
- Aplica regras de negócio
- Controla transações
export class CriarReservaUseCase {
constructor(
private readonly repoReservas: ReservaRepository, // Interface
private readonly publicador: PublicadorMensagem, // Interface
private readonly servicoDisponibilidade: ServicoDisponibilidade // Interface
) {}
async executar(input: CriarReservaInput): Promise<Reserva> {
// ✅ 1. Verificar conflitos
const conflitos = await this.repoReservas.buscarConflitos(
input.restauranteId,
input.dataHora,
input.numeroPessoas
);
if (conflitos.length > 0) {
throw new ConflitoReservaError();
}
// ✅ 2. Verificar disponibilidade via serviço externo
const disponivel = await this.servicoDisponibilidade.verificarVaga(
input.restauranteId,
input.dataHora,
input.numeroPessoas
);
if (!disponivel) {
throw new VagaIndisponivelError();
}
// ✅ 3. Criar entidade (regras aplicadas automaticamente)
const reserva = Reserva.criar(input);
// ✅ 4. Salvar no repositório
await this.repoReservas.salvar(reserva);
// ✅ 5. Avisar outros sistemas via evento
await this.publicador.publicar(
'confirmacoes-reserva',
reserva.gerarEvento()
);
return reserva;
}
}
4º Andar: Interfaces (As Portarias)
Função: Receber requisições do mundo exterior
É como as diferentes portarias de entrada do prédio:
- Portaria principal (Lambda API)
- Portaria de funcionários (Worker)
- Portaria de serviços (Cron Jobs)
// Portaria HTTP (Lambda)
export const criarReservaHandler = async (event: APIGatewayEvent) => {
const container = criarContainerDI();
const casoUso = container.obter<CriarReservaUseCase>('CriarReservaUseCase');
try {
const input = EsquemaCriarReserva.parse(JSON.parse(event.body));
const reserva = await casoUso.executar(input);
return respostaSucesso(201, ReservaDTO.doDominio(reserva));
} catch (erro) {
return tratarErroDominio(erro);
}
};
// Portaria de Mensagens (Worker)
export class ConsumidorConfirmacaoReserva {
constructor(private readonly casoUso: EnviarConfirmacaoReservaUseCase) {}
async processarMensagem(mensagem: MensagemSQS): Promise<void> {
const evento = EsquemaEventoReserva.parse(mensagem.body);
await this.casoUso.executar({
reservaId: evento.reservaId,
emailCliente: evento.emailCliente,
restauranteId: evento.restauranteId,
});
}
}
Comunicação Entre Departamentos (Módulos)
Como os Departamentos Se Comunicam
| Situação | Estratégia | Exemplo |
|---|---|---|
| Dados Críticos em Tempo Real | Via caso de uso público | Fidelidade consulta dados de Cliente |
| Notificações e Side Effects | Via eventos assíncronos | Reserva confirmada → enviar WhatsApp |
| Relatórios e Consultas | Via SQL direto | Dashboard com dados de múltiplos módulos |
Eventos de Domínio (Comunicação Assíncrona)
// ✅ Quando reserva é criada, outros módulos reagem
export class ReservaCriadaEvent {
constructor(
public readonly reservaId: string,
public readonly restauranteId: string,
public readonly clienteEmail: string,
public readonly dataHora: Date,
public readonly ocorreuEm: Date = new Date()
) {}
}
// Outros módulos ouvem e reagem:
// 📱 Módulo Messaging: envia confirmação WhatsApp
// 💎 Módulo Fidelidade: adiciona pontos ao cliente
// 📊 Módulo Analytics: registra métrica de reserva
Tratamento de Erros de Negócio
Hierarquia de Erros Clara
// Base para todos os erros de domínio
export abstract class ErroDominio extends Error {
abstract readonly codigo: string;
abstract readonly statusHTTP: number;
}
// Erros específicos do domínio de reservas
export class ConflitoReservaError extends ErroDominio {
readonly codigo = 'CONFLITO_RESERVA';
readonly statusHTTP = 409;
constructor() {
super('Já existe uma reserva para este horário');
}
}
export class DataReservaInvalidaError extends ErroDominio {
readonly codigo = 'DATA_INVALIDA';
readonly statusHTTP = 400;
constructor(mensagem: string) {
super(mensagem);
}
}
Tratamento no Handler (Interface)
function tratarErroDominio(erro: unknown): APIGatewayProxyResult {
if (erro instanceof ErroDominio) {
return {
statusCode: erro.statusHTTP,
body: JSON.stringify({
erro: erro.codigo,
mensagem: erro.message,
})
};
}
// Log de erro inesperado + resposta genérica
logger.error('Erro inesperado', { erro });
return {
statusCode: 500,
body: JSON.stringify({ erro: 'ERRO_INTERNO' })
};
}
Estratégia de Testes
Pirâmide de Testes (Foco no que Importa)
| Tipo | Foco | Quantidade | Ferramentas |
|---|---|---|---|
| Unit (maioria) | Domínio + casos de uso | > 80% | Vitest + mocks |
| Integration | Repositórios + Prisma | Casos críticos | Vitest + test DB |
| Contract | Eventos entre módulos | Interfaces públicas | Zod + Pact |
| E2E (poucos) | Fluxos principais | Jornadas críticas | Playwright |
Exemplo: Teste de Caso de Uso
describe('CriarReservaUseCase', () => {
let casoUso: CriarReservaUseCase;
let mockRepo: jest.Mocked<ReservaRepository>;
let mockPublicador: jest.Mocked<PublicadorMensagem>;
beforeEach(() => {
mockRepo = {
salvar: jest.fn(),
buscarConflitos: jest.fn().mockResolvedValue([]),
};
mockPublicador = {
publicar: jest.fn(),
};
casoUso = new CriarReservaUseCase(mockRepo, mockPublicador);
});
it('deve criar reserva quando não há conflitos', async () => {
// Arrange
const input = {
restauranteId: 'rest-123',
dadosCliente: dadosClienteValidos,
dataHora: addDays(new Date(), 1),
numeroPessoas: 4,
};
// Act
const reserva = await casoUso.executar(input);
// Assert
expect(reserva.status).toBe(StatusReserva.PENDENTE);
expect(mockRepo.salvar).toHaveBeenCalledWith(reserva);
expect(mockPublicador.publicar).toHaveBeenCalledWith(
'confirmacoes-reserva',
expect.any(ReservaCriadaEvent)
);
});
it('deve lançar erro quando há conflitos', async () => {
// Arrange
mockRepo.buscarConflitos.mockResolvedValue([reservaExistente]);
// Act & Assert
await expect(casoUso.executar(inputValido))
.rejects.toThrow(ConflitoReservaError);
});
});
Vantagens da Clean Architecture
Por que Essa Organização Funciona?
| Vantagem | Explicação | Exemplo Prático |
|---|---|---|
| Testabilidade | Domínio testável sem infraestrutura | Testa regras de negócio sem banco rodando |
| Flexibilidade | Trocar tecnologia sem afetar regras | Migrar de Prisma para TypeORM = só camada infra |
| Manutenibilidade | Responsabilidades claras por camada | Bug de validação? Está sempre no Domain |
| Evolução Independente | Módulos evoluem sem se afetar | Melhorar Fidelidade não quebra Reservas |
Fluxo Completo de uma Funcionalidade
// 1. INTERFACE: Recebe requisição
export const criarReservaHandler = async (event: APIGatewayEvent) => {
const casoUso = container.obter<CriarReservaUseCase>('CriarReservaUseCase');
const input = validarInput(event.body);
const reserva = await casoUso.executar(input); // → Application
return respostaSucesso(201, reserva);
};
// 2. APPLICATION: Orquestra o trabalho
class CriarReservaUseCase {
async executar(input) {
const reserva = Reserva.criar(input); // → Domain
await this.repo.salvar(reserva); // → Infrastructure
await this.publicador.publicar(evento); // → Infrastructure
return reserva;
}
}
// 3. DOMAIN: Aplica regras de negócio
class Reserva {
static criar(input) {
if (input.dataHora.ePassado()) throw new ErroDataInvalida();
return new Reserva(/* ... */);
}
}
// 4. INFRASTRUCTURE: Executa operação técnica
class PrismaReservaRepository {
async salvar(reserva: Reserva) {
await this.prisma.reserva.create(/* ... */);
}
}
Checklist: Como Criar Novo Caso de Uso
- ✅ Modelar entidade no
domain/com suas regras - ✅ Definir interfaces necessárias (repositórios, publishers)
- ✅ Escrever caso de uso em
application/ - ✅ Implementar adapters em
infrastructure/(Prisma, SQS...) - ✅ Configurar injeção de dependências no container
- ✅ Criar handler (Lambda) ou consumer (Worker) em
interfaces/ - ✅ Garantir isolamento por
restauranteId(multi-tenant) - ✅ Escrever testes (domínio + integração)
- ✅ Emitir eventos se outros módulos precisam saber
Resumo: O Poder da Clean Architecture
A Transformação
ANTES (Código Acoplado):
Mudança no banco → quebra API → quebra frontend → quebra tudo! 💥
DEPOIS (Clean Architecture):
Mudança no banco → só troca adapter → resto funciona normal! ✅
Benefícios Reais
- Equipe cresce mais rápido: Novos devs entendem padrões claros
- Menos bugs: Regras centralizadas no domínio
- Evolução segura: Pode modernizar tecnologia sem medo
- Testes confiáveis: Domínio testado independentemente
Clean Architecture coloca o negócio no centro e torna tecnologia um detalhe intercambiável. É a base que permite ao BITIO crescer sem perder qualidade.