Ferramentas & Produtividade

Boas Práticas de Clean Architecture para Times Ágeis

8 min de leitura

Boas Práticas de Clean Architecture para Times Ágeis

Clean Architecture para Times Ágeis: Fundamentação e Benefícios Clean Architecture é um paradigma que organiza o código em camadas independentes, onde cada uma tem responsabilidades bem definidas. Diferente de arquiteturas tradicionais monolíticas, ela permite que times ágeis façam mudanças rápidas sem comprometer a estabilidade do sistema. A ideia central é que o núcleo da aplicação (lógica de negócio) não dependa de detalhes de implementação como frameworks, bancos de dados ou interfaces web. Para times ágeis, isso significa que você pode mudar de tecnologia, ajustar requisitos ou refatorar sem reescrever tudo. O código fica testável, legível e mantível. A estrutura típica consiste em quatro camadas: Entities (regras de negócio essenciais), Use Cases (casos de uso da aplicação), Interface Adapters (controllers, gateways, presenters) e Frameworks & Drivers (frameworks, bancos de dados, web). Estrutura Prática em Camadas Organizando seu Projeto Um projeto Clean bem estruturado segue este padrão de pastas: Veja um exemplo funcional em TypeScript: A separação clara permite que o time

<h2>Clean Architecture para Times Ágeis: Fundamentação e Benefícios</h2>

<p>Clean Architecture é um paradigma que organiza o código em camadas independentes, onde cada uma tem responsabilidades bem definidas. Diferente de arquiteturas tradicionais monolíticas, ela permite que times ágeis façam mudanças rápidas sem comprometer a estabilidade do sistema. A ideia central é que o núcleo da aplicação (lógica de negócio) não dependa de detalhes de implementação como frameworks, bancos de dados ou interfaces web.</p>

<p>Para times ágeis, isso significa que você pode mudar de tecnologia, ajustar requisitos ou refatorar sem reescrever tudo. O código fica testável, legível e mantível. A estrutura típica consiste em quatro camadas: Entities (regras de negócio essenciais), Use Cases (casos de uso da aplicação), Interface Adapters (controllers, gateways, presenters) e Frameworks &amp; Drivers (frameworks, bancos de dados, web).</p>

<h2>Estrutura Prática em Camadas</h2>

<h3>Organizando seu Projeto</h3>

<p>Um projeto Clean bem estruturado segue este padrão de pastas:</p>

<pre><code>src/

├── domain/ # Entities e regras de negócio

│ └── user/

│ └── User.ts

├── application/ # Use Cases

│ └── user/

│ └── CreateUserUseCase.ts

├── interface/ # Controllers e Presenters

│ ├── controllers/

│ │ └── UserController.ts

│ └── presenters/

│ └── UserPresenter.ts

└── infrastructure/ # BD, APIs externas

├── repositories/

│ └── UserRepository.ts

└── http/

└── express.config.ts</code></pre>

<p>Veja um exemplo funcional em TypeScript:</p>

<pre><code class="language-typescript">// domain/user/User.ts - Entidade pura, sem dependências

export class User {

constructor(

readonly id: string,

readonly email: string,

readonly name: string

) {

this.validate();

}

private validate(): void {

if (!this.email.includes(&#039;@&#039;)) {

throw new Error(&#039;Email inválido&#039;);

}

}

}

// application/user/CreateUserUseCase.ts - Caso de uso

export interface UserRepository {

save(user: User): Promise&lt;void&gt;;

findByEmail(email: string): Promise&lt;User | null&gt;;

}

export class CreateUserUseCase {

constructor(private userRepository: UserRepository) {}

async execute(email: string, name: string): Promise&lt;User&gt; {

const existing = await this.userRepository.findByEmail(email);

if (existing) {

throw new Error(&#039;Usuário já existe&#039;);

}

const user = new User(crypto.randomUUID(), email, name);

await this.userRepository.save(user);

return user;

}

}

// interface/controllers/UserController.ts - Adapter HTTP

export class UserController {

constructor(private createUserUseCase: CreateUserUseCase) {}

async handle(request: any, response: any): Promise&lt;void&gt; {

const { email, name } = request.body;

try {

const user = await this.createUserUseCase.execute(email, name);

response.status(201).json({

id: user.id,

email: user.email,

name: user.name

});

} catch (error) {

response.status(400).json({ error: error.message });

}

}

}

// infrastructure/repositories/UserRepository.ts - Implementação concreta

import { PrismaClient } from &#039;@prisma/client&#039;;

export class PrismaUserRepository implements UserRepository {

constructor(private prisma: PrismaClient) {}

async save(user: User): Promise&lt;void&gt; {

await this.prisma.user.create({

data: {

id: user.id,

email: user.email,

name: user.name

}

});

}

async findByEmail(email: string): Promise&lt;User | null&gt; {

const data = await this.prisma.user.findUnique({ where: { email } });

return data ? new User(data.id, data.email, data.name) : null;

}

}</code></pre>

<p>A separação clara permite que o time trabalhe em paralelo: frontend mexe em controllers, backend em use cases, e infraestrutura em repositórios — tudo sem conflitos.</p>

<h2>Injeção de Dependências e Testabilidade</h2>

<h3>Por Que DI Importa em Times Ágeis</h3>

<p>Dependency Injection é essencial para Clean Architecture. Ela desacopla componentes e torna testes unitários triviais. Em vez de suas classes criarem suas próprias dependências, elas as recebem. Isso permite mockar qualquer coisa em testes sem reescrever código de produção.</p>

<pre><code class="language-typescript">// Teste unitário simples com DI

describe(&#039;CreateUserUseCase&#039;, () =&gt; {

let useCase: CreateUserUseCase;

let mockRepository: jest.Mocked&lt;UserRepository&gt;;

beforeEach(() =&gt; {

// Mock da dependência - sem banco real

mockRepository = {

save: jest.fn(),

findByEmail: jest.fn()

};

useCase = new CreateUserUseCase(mockRepository);

});

it(&#039;deve criar usuário com email válido&#039;, async () =&gt; {

mockRepository.findByEmail.mockResolvedValue(null);

const user = await useCase.execute(&#039;test@example.com&#039;, &#039;João&#039;);

expect(user.email).toBe(&#039;test@example.com&#039;);

expect(mockRepository.save).toHaveBeenCalledWith(user);

});

it(&#039;deve rejeitar email duplicado&#039;, async () =&gt; {

const existingUser = new User(&#039;1&#039;, &#039;test@example.com&#039;, &#039;João&#039;);

mockRepository.findByEmail.mockResolvedValue(existingUser);

await expect(

useCase.execute(&#039;test@example.com&#039;, &#039;Maria&#039;)

).rejects.toThrow(&#039;Usuário já existe&#039;);

});

});</code></pre>

<p>Com DI, um time pode testar 95% do código sem subir banco de dados, cache ou APIs externas. Isso acelera CI/CD — testes rodam em segundos, não minutos.</p>

<h2>Boas Práticas para Agilidade</h2>

<h3>Princípios que Importam</h3>

<blockquote><p><strong>SOLID é seu amigo:</strong> Single Responsibility (cada classe um propósito), Open/Closed (aberto para extensão, fechado para modificação), Liskov Substitution (interfaces respeitadas), Interface Segregation (interfaces pequenas), Dependency Inversion (dependa de abstrações).</p></blockquote>

<p>Praticamente, isso significa: se você precisa adicionar um novo método de pagamento (Stripe, PayPal, Bitcoin), adiciona um novo adapter sem tocar em código existente. Use interfaces genéricas:</p>

<pre><code class="language-typescript">// Abstração que permite múltiplas implementações

export interface PaymentGateway {

process(amount: number, currency: string): Promise&lt;string&gt;; // retorna ID da transação

}

// Implementações podem ser adicionadas sem modificar use case

export class StripePaymentGateway implements PaymentGateway {

async process(amount: number, currency: string): Promise&lt;string&gt; {

// lógica Stripe

return &#039;tx_123&#039;;

}

}

export class PayPalPaymentGateway implements PaymentGateway {

async process(amount: number, currency: string): Promise&lt;string&gt; {

// lógica PayPal

return &#039;tx_456&#039;;

}

}

// Use case é agnóstico da implementação

export class ProcessPaymentUseCase {

constructor(private gateway: PaymentGateway) {}

async execute(amount: number): Promise&lt;string&gt; {

return this.gateway.process(amount, &#039;BRL&#039;);

}

}</code></pre>

<p>Outra prática essencial: <strong>versionamento de APIs e contratos</strong>. Em times ágeis, requisitos mudam. Mantenha contracts claros entre camadas:</p>

<pre><code class="language-typescript">// Contrato imutável entre camadas

export interface UserOutputDto {

id: string;

email: string;

name: string;

}

// Controllers retornam DTOs, não entidades

export class UserPresenter {

static toOutput(user: User): UserOutputDto {

return {

id: user.id,

email: user.email,

name: user.name

};

}

}</code></pre>

<p>Isso garante que mudanças internas não quebrem o contrato com quem consome sua API ou serviço.</p>

<h2>Conclusão</h2>

<p>Você aprendeu três pilares: <strong>(1) Organize em camadas independentes</strong> — domain, application, interface, infrastructure — para que mudanças em uma não cascateiem nas outras; <strong>(2) Use injeção de dependências e abstrações</strong> — torne testes rápidos e desacople tecnologias; <strong>(3) Respeite contratos entre camadas</strong> — DTOs, interfaces e versionamento garantem evolução sem quebra.</p>

<p>Clean Architecture + times ágeis = velocidade sem débito técnico. Comece pequeno, refatore conforme cresce, e seu código permanecerá limpo.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://www.oreilly.com/library/view/clean-architecture-a/9780134494272/" target="_blank" rel="noopener noreferrer">Clean Architecture: A Craftsman&#039;s Guide to Software Structure and Design</a> — Robert C. Martin</li>

<li><a href="https://en.wikipedia.org/wiki/Dependency_inversion_principle" target="_blank" rel="noopener noreferrer">The Dependency Inversion Principle</a> — SOLID Principles</li>

<li><a href="https://docs.nestjs.com/" target="_blank" rel="noopener noreferrer">NestJS Documentation - Architecture</a> — Framework que implementa Clean Architecture nativamente</li>

<li><a href="https://www.domainlanguage.com/ddd/" target="_blank" rel="noopener noreferrer">Domain-Driven Design</a> — Eric Evans</li>

<li><a href="https://www.pearson.com/en-us/subject-catalog/p/test-driven-development-by-example/P200000009356" target="_blank" rel="noopener noreferrer">Test Driven Development: By Example</a> — Kent Beck</li>

</ul>

Comentários

Mais em Ferramentas & Produtividade

CI/CD na Prática
CI/CD na Prática

O que é CI/CD e Por Que Importa CI/CD significa Integração Contínua (CI) e En...

Mensageria Assíncrona na Prática
Mensageria Assíncrona na Prática

O que é Mensageria Assíncrona e Por Que Importa Mensageria assíncrona é um pa...

Como Usar Arquitetura de Software em Produção
Como Usar Arquitetura de Software em Produção

Como Usar Arquitetura de Software em Produção Fundamentos de Arquitetura em A...