Pular para o conteúdo

Design Patterns Hexagonal: entendendo de forma simples e prática

Se você já sentiu que seu projeto ficou difícil de manter, testar ou evoluir com o tempo, provavelmente o problema não foi a linguagem ou o framework, mas sim a forma como a arquitetura foi pensada. É exatamente nesse ponto que o Design Pattern Hexagonal, também conhecido como Ports and Adapters, entra em cena. Ele é um padrão de arquitetura que ajuda a organizar o código de forma mais clara, desacoplada e preparada para mudanças — algo essencial no mundo real do desenvolvimento de software.

A ideia central da arquitetura hexagonal é simples: separar completamente a regra de negócio do restante do sistema, como banco de dados, APIs externas, filas, interfaces web ou mobile. Em vez de o domínio “conhecer” esses detalhes, tudo se comunica por meio de portas (interfaces) e adaptadores. Isso torna o sistema mais testável, mais flexível e muito mais fácil de evoluir.

Se você trabalha com Node.js e NestJS, boas notícias: o NestJS se encaixa muito bem com o conceito de arquitetura hexagonal, principalmente por incentivar o uso de módulos, injeção de dependência e interfaces. Ao longo deste artigo, vamos ver exemplos práticos de como aplicar o padrão hexagonal usando NestJS, sempre com uma abordagem clara e amigável para quem está começando.

Nosso objetivo é justamente descomplicar conceitos que parecem avançados à primeira vista. Não importa se você é iniciante ou já desenvolve há algum tempo: entender o Design Pattern Hexagonal pode mudar completamente a forma como você estrutura seus projetos e encara a manutenção do código no dia a dia.

O que é o Design Pattern Hexagonal na prática?

Antes de pensar em código, frameworks ou pastas, é importante entender a ideia por trás do Design Pattern Hexagonal. Diferente de muitos padrões que focam em resolver problemas específicos de código, a arquitetura hexagonal olha para o sistema como um todo e propõe uma forma mais saudável de organizar responsabilidades.

Na prática, o padrão hexagonal diz o seguinte:
o coração da aplicação deve ser o domínio, ou seja, as regras de negócio. Tudo o que é externo — banco de dados, API REST, filas, serviços de terceiros, interface web ou mobile — deve ficar “ao redor” desse núcleo, sem acoplamento direto.

O nome “hexagonal” não é sobre a forma do código, mas sim uma metáfora visual. Imagine o domínio no centro e, ao redor, vários lados (ou portas) por onde o sistema se comunica com o mundo externo. Cada lado representa uma porta, que é basicamente uma interface. Quem implementa essa interface é um adaptador.

Portas e Adaptadores, sem complicação

  • Portas: são contratos (interfaces) que definem como o domínio se comunica.
    Exemplo: uma interface para salvar um usuário, buscar pedidos ou enviar uma notificação.
  • Adaptadores: são implementações dessas portas.
    Exemplo: um adaptador que usa PostgreSQL, outro que usa MongoDB, ou até um mock para testes.

O ponto mais importante aqui é que o domínio não sabe quem está do outro lado. Ele apenas conhece a interface. Isso significa que você pode trocar o banco de dados, mudar de REST para GraphQL ou até alterar o framework sem precisar reescrever suas regras de negócio.

Por que isso é tão poderoso?

Em projetos tradicionais, é comum ver regras de negócio misturadas com chamadas de banco, validações de HTTP e detalhes de framework. Isso funciona no começo, mas vira um problema com o tempo. Já com a arquitetura hexagonal:

  • O código fica mais organizado e legível
  • Testar regras de negócio se torna muito mais simples
  • Mudanças externas não quebram o coração da aplicação
  • O sistema cresce de forma mais controlada

No contexto do NestJS, esse padrão faz ainda mais sentido, porque o framework já incentiva boas práticas como injeção de dependência e separação de camadas.

Principais componentes da Arquitetura Hexagonal

Agora que a ideia geral já está clara, vamos entender quais são os componentes que formam a arquitetura hexagonal e qual é o papel de cada um. Não se preocupe com termos difíceis — a proposta aqui é manter tudo simples e prático, do jeito que usamos no dia a dia em projetos reais.

1. Domínio (o coração da aplicação)

O domínio é a parte mais importante do sistema. É aqui que ficam as regras de negócio, totalmente independentes de framework, banco de dados ou protocolo de comunicação.

No domínio você normalmente encontra:

  • Entidades (User, Order, Product, etc.)
  • Value Objects
  • Regras e validações de negócio
  • Interfaces (portas)

O domínio não deve importar nada do NestJS, nem de ORM, nem de HTTP. Ele precisa ser puro. Isso garante que ele possa ser testado facilmente e reaproveitado se necessário.

2. Portas (interfaces)

As portas são contratos que o domínio define para se comunicar com o mundo externo. Elas dizem o que precisa ser feito, mas não como.

Exemplos de portas:

  • UserRepository
  • PaymentService
  • NotificationGateway

Essas interfaces ficam próximas do domínio, pois são parte da regra de negócio, não da infraestrutura.

3. Adaptadores (infraestrutura)

Os adaptadores são responsáveis por implementar as portas. Aqui entram:

  • Banco de dados (PostgreSQL, MongoDB, Prisma, TypeORM)
  • APIs externas
  • Filas (RabbitMQ, SQS)
  • Controllers HTTP

Eles conhecem o domínio, mas o domínio não conhece eles. Essa inversão de dependência é um dos pilares do padrão hexagonal.

4. Aplicação (casos de uso)

A camada de aplicação conecta tudo. Ela orquestra os casos de uso, chamando entidades e portas do domínio. Não é regra de negócio pura, mas também não é infraestrutura.

Exemplo de caso de uso:

  • Criar usuário
  • Processar pagamento
  • Cancelar pedido

No NestJS, essa camada costuma se encaixar muito bem nos services, desde que eles não fiquem cheios de lógica de infraestrutura.

Como tudo se conecta?

O fluxo geralmente funciona assim:
Controller → Caso de uso → Domínio → Porta → Adaptador

Essa separação deixa o sistema mais previsível, organizado e fácil de manter.

Estrutura de pastas no NestJS usando Arquitetura Hexagonal

Uma das dúvidas mais comuns quando alguém começa a estudar arquitetura hexagonal é: “onde eu coloco cada coisa no projeto?”. A boa notícia é que não existe uma única estrutura perfeita, mas existem organizações que funcionam muito bem, especialmente com NestJS.

Aqui a ideia não é criar algo engessado, e sim uma estrutura clara, fácil de entender e de escalar.

Uma estrutura simples e funcional

Abaixo está um exemplo de organização baseada em domínio, bem alinhada com o padrão hexagonal:

src/
├── domain/
│   ├── entities/
│   │   └── user.entity.ts
│   ├── ports/
│   │   └── user-repository.port.ts
│   └── use-cases/
│       └── create-user.usecase.ts
│
├── application/
│   └── services/
│       └── create-user.service.ts
│
├── infrastructure/
│   ├── database/
│   │   └── user.repository.ts
│   ├── http/
│   │   └── user.controller.ts
│   └── modules/
│       └── user.module.ts
│
└── main.ts

Vamos entender isso por partes.

Domain: regras de negócio puras

Tudo o que está dentro de domain não conhece o NestJS. Aqui ficam as entidades, portas e casos de uso. Se você quisesse reaproveitar essa lógica em outro projeto ou até em outra linguagem, esse seria o ponto de partida.

Application: orquestração

A camada de application serve como uma ponte entre o domínio e a infraestrutura. Ela coordena os casos de uso, aplicando regras e chamando as portas necessárias.

Em projetos menores, essa camada pode até ser mesclada com o domínio, mas separá-la ajuda muito quando o sistema cresce.

Infrastructure: detalhes técnicos

Aqui entram os detalhes que mudam com mais frequência:

  • Controllers (REST, GraphQL)
  • Implementações de repositórios
  • ORM
  • Integrações externas

Essas classes implementam as interfaces definidas no domínio.

E o NestJS entra onde?

O NestJS fica principalmente na infraestrutura, conectando tudo por meio da injeção de dependência. Ele sabe qual adaptador usar para cada porta, mas o domínio continua isolado.

Exemplo prático: começando pelo domínio no NestJS

Agora vamos sair um pouco da teoria e partir para um exemplo prático, do jeito que acontece no dia a dia. A melhor forma de aplicar a arquitetura hexagonal é sempre começar pelo domínio, sem pensar ainda em controller, banco de dados ou framework.

Vamos imaginar um caso simples: criação de usuários.

Criando a entidade User

No domínio, a entidade representa o conceito central do negócio. Ela contém dados e regras que fazem sentido para o problema, não para o banco ou para a API.

// src/domain/entities/user.entity.ts
export class User {
  constructor(
    public readonly id: string,
    public name: string,
    public email: string,
  ) {
    if (!email.includes('@')) {
      throw new Error('Email inválido');
    }
  }
}

Perceba que:

  • Não existe decorator do NestJS
  • Não existe ORM
  • A validação faz parte da regra de negócio

Criando a porta (interface do repositório)

Agora o domínio precisa salvar esse usuário em algum lugar. Mas ele não quer saber onde. Por isso criamos uma porta.

// src/domain/ports/user-repository.port.ts
import { User } from '../entities/user.entity';

export interface UserRepository {
  save(user: User): Promise<void>;
  findByEmail(email: string): Promise<User | null>;
}

Essa interface define o contrato. O domínio só conhece isso.

Criando o caso de uso

O caso de uso é quem orquestra a regra. Ele recebe dados, aplica validações e usa as portas.

// src/domain/use-cases/create-user.usecase.ts
import { UserRepository } from '../ports/user-repository.port';
import { User } from '../entities/user.entity';
import { randomUUID } from 'crypto';

export class CreateUserUseCase {
  constructor(private readonly userRepository: UserRepository) {}

  async execute(name: string, email: string): Promise<User> {
    const existingUser = await this.userRepository.findByEmail(email);

    if (existingUser) {
      throw new Error('Usuário já existe');
    }

    const user = new User(randomUUID(), name, email);
    await this.userRepository.save(user);

    return user;
  }
}

Note como esse código:

  • Não depende de banco de dados
  • Não depende de HTTP
  • Pode ser testado com mocks facilmente

Até aqui, nenhum NestJS apareceu, e isso é proposital. Essa é a essência da arquitetura hexagonal.

Implementando os adaptadores na infraestrutura com NestJS

Agora que o domínio está bem definido, chegou a hora de conectar o mundo externo a ele. É aqui que entram os adaptadores, que vivem na camada de infraestrutura e conhecem tanto o NestJS quanto as portas definidas no domínio.

Vamos continuar com o exemplo de criação de usuários.

Implementando o repositório (adaptador de saída)

Esse adaptador é responsável por falar com o banco de dados. Para simplificar, vamos usar um repositório em memória, mas a ideia é exatamente a mesma para Prisma, TypeORM ou qualquer outro ORM.

// src/infrastructure/database/user.repository.ts
import { Injectable } from '@nestjs/common';
import { UserRepository } from '../../domain/ports/user-repository.port';
import { User } from '../../domain/entities/user.entity';

@Injectable()
export class InMemoryUserRepository implements UserRepository {
  private users: User[] = [];

  async save(user: User): Promise<void> {
    this.users.push(user);
  }

  async findByEmail(email: string): Promise<User | null> {
    return this.users.find(user => user.email === email) || null;
  }
}

Aqui sim:

  • Usamos @Injectable()
  • Implementamos a porta do domínio
  • O domínio continua sem saber que isso existe

Criando o Controller (adaptador de entrada)

O controller é outro tipo de adaptador. Ele traduz uma requisição HTTP para um formato que o domínio entende.

// src/infrastructure/http/user.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { CreateUserUseCase } from '../../domain/use-cases/create-user.usecase';

@Controller('users')
export class UserController {
  constructor(
    private readonly createUserUseCase: CreateUserUseCase,
  ) {}

  @Post()
  async create(@Body() body: { name: string; email: string }) {
    return this.createUserUseCase.execute(body.name, body.email);
  }
}

O controller:

  • Não tem regra de negócio
  • Apenas repassa os dados
  • Traduz HTTP para o caso de uso

Conectando tudo no módulo

O NestJS é responsável por “colar” as peças usando injeção de dependência.

// src/infrastructure/modules/user.module.ts
import { Module } from '@nestjs/common';
import { UserController } from '../http/user.controller';
import { InMemoryUserRepository } from '../database/user.repository';
import { CreateUserUseCase } from '../../domain/use-cases/create-user.usecase';

@Module({
  controllers: [UserController],
  providers: [
    CreateUserUseCase,
    {
      provide: 'UserRepository',
      useClass: InMemoryUserRepository,
    },
  ],
})
export class UserModule {}

Em projetos reais, você pode usar tokens, símbolos ou classes abstratas para representar as portas.

Vantagens e desafios da Arquitetura Hexagonal em projetos reais

Depois de ver a teoria e um exemplo prático com NestJS, é natural surgir a pergunta: vale mesmo a pena usar arquitetura hexagonal no dia a dia? A resposta curta é: depende do contexto. Como quase tudo em software, existem vantagens claras, mas também alguns desafios que precisam ser considerados.

Principais vantagens

A primeira grande vantagem é o baixo acoplamento. Como o domínio não depende de detalhes externos, trocar um banco de dados, mudar um ORM ou até substituir uma API REST por GraphQL se torna muito mais simples. Isso reduz bastante o medo de mudanças no projeto.

Outro ponto forte é a testabilidade. Como as regras de negócio estão isoladas, você consegue testar casos de uso sem subir servidor, sem banco de dados e sem mocks complexos de framework. Isso melhora a qualidade do código e acelera o feedback durante o desenvolvimento.

A clareza de responsabilidades também é um destaque. Cada parte do sistema tem um papel bem definido:

  • Domínio cuida das regras
  • Aplicação orquestra
  • Infraestrutura resolve detalhes técnicos

Em times, isso ajuda muito na comunicação e na manutenção do código ao longo do tempo.

Principais desafios

O principal desafio para quem está começando é a complexidade inicial. Para projetos muito pequenos, a arquitetura hexagonal pode parecer exagerada. Criar interfaces, casos de uso e adaptadores exige disciplina e entendimento do problema.

Outro ponto é a curva de aprendizado. Desenvolvedores iniciantes podem estranhar o fato de o controller não ter lógica nenhuma ou o domínio não conhecer o banco de dados. Isso é normal e faz parte do processo.

Além disso, se o time não seguir o padrão corretamente, existe o risco de misturar responsabilidades e perder os benefícios da arquitetura.

Quando usar?

A arquitetura hexagonal brilha em:

  • Sistemas que vão crescer
  • Projetos com regras de negócio complexas
  • Aplicações que precisam mudar com frequência

Conclusão

A arquitetura hexagonal não é uma moda nem uma regra obrigatória, mas sim uma ferramenta poderosa para quem quer construir software mais organizado, testável e preparado para mudanças. Ao longo deste artigo, vimos que a ideia central é simples: proteger o domínio e evitar que as regras de negócio fiquem reféns de detalhes técnicos como frameworks, bancos de dados ou protocolos de comunicação.

Usando NestJS, esse padrão se encaixa de forma muito natural. A injeção de dependência, a separação em módulos e o incentivo ao uso de boas práticas tornam a aplicação da arquitetura hexagonal muito mais tranquila do que parece à primeira vista. Começando pelo domínio, criando portas bem definidas e implementando adaptadores claros, você ganha um sistema mais previsível e fácil de manter.

Claro, nem todo projeto precisa nascer 100% hexagonal. Em aplicações menores, você pode aplicar os conceitos aos poucos: separar melhor as regras de negócio, evitar lógica nos controllers e usar interfaces para reduzir acoplamento. Com o tempo, essa mentalidade já faz uma enorme diferença na qualidade do código.

Aqui no CulturaDev, a ideia é justamente essa: ajudar você a evoluir como desenvolvedor, entendendo não só como escrever código, mas por que certas decisões arquiteturais fazem sentido. A arquitetura hexagonal é um ótimo próximo passo para quem já domina o básico e quer subir o nível profissional.

Agora quero saber de você: você já tentou aplicar a arquitetura hexagonal em algum projeto? Teve dificuldades ou viu benefícios claros? Deixe seu comentário abaixo e compartilhe sua experiência. Essa troca é o que faz a comunidade crescer.