Dominando a Clean Architecture: Guia Definitivo de Arquitetura de Software

Dominando a Clean Architecture: Guia Definitivo de Arquitetura de Software






Dominando a Arquitetura de Clean Architecture


Dominando a Arquitetura de Clean Architecture

Princípios, camadas e práticas para manter domínio estável, código testável e evolução segura


1. Fundamentos essenciais da Clean Architecture

Ao longo da minha prática, a Clean Architecture voltou-se uma bússola para manter o código sustentável diante de mudanças. O foco é preservar o domínio como núcleo, isolando-o de frameworks, detalhes de infraestrutura e interfaces externas.

  • Domínio no centro: entidades e regras de negócio que não devem depender de tecnologias específicas.
  • Dependências que fluem inward: camadas externas podem depender de interfaces definidas pelo núcleo, nunca o contrário.
  • Separa o que é domínio do que é entrega: define claramente camadas de aplicação, interfaces e infraestrutura.
  • Testabilidade elevada: possibilidades de isolar o domínio para testes unitários sem dependências de UI, bancos ou redes.

2. Camadas e responsabilidades

A arquitetura tipicamente organiza-se em camadas concêntricas. Cada camada consome apenas aquelas acima dela que são estáveis e definidas por interfaces. Em termos práticos, as camadas costumam ser:

  • Entidades (Domínio): regras de negócio e objetos que representam o modelo de negócio.
  • Casos de Uso (Aplicação): coordena fluxos de negócios, orquestra entidades e validações de regras aplicáveis.
  • Adaptadores de Interface (Presenters, Controllers, Gateways): traduz entre o domínio e o mundo externo (UI, API, bancos, serviços).
  • Frameworks e Drivers (Infraestrutura): bancos de dados, UI, serviços externos, bibliotecas de terceiros.

Regra de dependência destaca que código interno não deve importar detalhes da infraestrutura. Em vez disso, dependemos de interfaces que o mundo externo implementa tarde. Esse acoplamento mínimo facilita substituições, testes e evolução sem quebrar o domínio.

3. Interfaces, boundaries e inversão de dependência

As fronteiras (boundaries) definem como o interior expõe serviços aos consumidores externos. A inversão de dependência (DI) é o mecanismo que viabiliza essa comunicação sem criar acoplamento direto entre domínio e infra. Práticas comuns:

  • Defina interfaces para repositórios, gateways e serviços de domínio. O núcleo fornece as assinaturas, as implementações ficam no exterior.
  • Use Dependency Injection para injetar dependências em tempo de execução, favorecendo testes com mocks/fábricas.
  • Separa models de domínio (entidades) de DTOs/mapeadores usados pela interface com o mundo externo.
  • Adapte a entrada/saída (Controller/Presenter) para o formato desejado sem poluir o domínio com detalhes de transporte.

4. Guia rápido de implementação (exemplo em TypeScript)

Abaixo apresento um esqueleto simples que ilustra o alinhamento entre domínio, casos de uso e uma implementação de interface de repositório. Observe como o uso do caso de uso permanece independente da implementação de armazenamento.

// Domínio
class User {
  constructor(public id: string, public name: string) {}
}

// Boundaries (interfaces)
interface IUserRepository {
  save(user: User): Promise;
  findByName(name: string): Promise<User | null>;
}

// Utilitário simples de ID (sem dependências externas)
function generateId(): string {
  return Math.random().toString(36).substring(2, 9);
}

// Caso de uso (coordenador do fluxo)
class CreateUser {
  constructor(private userRepo: IUserRepository) {}

  async execute(name: string): Promise<User> {
    const user = new User(generateId(), name);
    await this.userRepo.save(user);
    return user;
  }
}

// Adapter: implementação concreta do repositório (infra)
class InMemoryUserRepository implements IUserRepository {
  private users: User[] = [];

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

  async findByName(name: string): Promise<User | null> {
    const found = this.users.find(u => u.name === name);
    return found ?? null;
  }
}

// Uso (injeção de dependência, no ponto de comutação com o mundo externo)
(async () => {
  const repo = new InMemoryUserRepository();
  const cc = new CreateUser(repo);

  const alice = await cc.execute("Alice");
  console.log("Criado:", alice);
})();

Gostou do conteúdo?

Aprofunde-se em mais posts sobre arquitetura de software. Explore materiais que ajudam a consolidar práticas sólidas e escaláveis.

Recomendo ler:

© 2026 Yurideveloper. All rights reserved.