Boas Práticas de Clean Architecture para Times Ágeis Já leu

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

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:

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

Veja um exemplo funcional em 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('@')) {
      throw new Error('Email inválido');
    }
  }
}

// application/user/CreateUserUseCase.ts - Caso de uso
export interface UserRepository {
  save(user: User): Promise<void>;
  findByEmail(email: string): Promise<User | null>;
}

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

  async execute(email: string, name: string): Promise<User> {
    const existing = await this.userRepository.findByEmail(email);
    if (existing) {
      throw new Error('Usuário já existe');
    }

    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<void> {
    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 '@prisma/client';

export class PrismaUserRepository implements UserRepository {
  constructor(private prisma: PrismaClient) {}

  async save(user: User): Promise<void> {
    await this.prisma.user.create({
      data: {
        id: user.id,
        email: user.email,
        name: user.name
      }
    });
  }

  async findByEmail(email: string): Promise<User | null> {
    const data = await this.prisma.user.findUnique({ where: { email } });
    return data ? new User(data.id, data.email, data.name) : null;
  }
}

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.

Injeção de Dependências e Testabilidade

Por Que DI Importa em Times Ágeis

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.

// Teste unitário simples com DI
describe('CreateUserUseCase', () => {
  let useCase: CreateUserUseCase;
  let mockRepository: jest.Mocked<UserRepository>;

  beforeEach(() => {
    // Mock da dependência - sem banco real
    mockRepository = {
      save: jest.fn(),
      findByEmail: jest.fn()
    };
    useCase = new CreateUserUseCase(mockRepository);
  });

  it('deve criar usuário com email válido', async () => {
    mockRepository.findByEmail.mockResolvedValue(null);

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

    expect(user.email).toBe('test@example.com');
    expect(mockRepository.save).toHaveBeenCalledWith(user);
  });

  it('deve rejeitar email duplicado', async () => {
    const existingUser = new User('1', 'test@example.com', 'João');
    mockRepository.findByEmail.mockResolvedValue(existingUser);

    await expect(
      useCase.execute('test@example.com', 'Maria')
    ).rejects.toThrow('Usuário já existe');
  });
});

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.

Boas Práticas para Agilidade

Princípios que Importam

SOLID é seu amigo: 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).

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:

// Abstração que permite múltiplas implementações
export interface PaymentGateway {
  process(amount: number, currency: string): Promise<string>; // 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<string> {
    // lógica Stripe
    return 'tx_123';
  }
}

export class PayPalPaymentGateway implements PaymentGateway {
  async process(amount: number, currency: string): Promise<string> {
    // lógica PayPal
    return 'tx_456';
  }
}

// Use case é agnóstico da implementação
export class ProcessPaymentUseCase {
  constructor(private gateway: PaymentGateway) {}

  async execute(amount: number): Promise<string> {
    return this.gateway.process(amount, 'BRL');
  }
}

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

// 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
    };
  }
}

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

Conclusão

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

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

Referências


Artigos relacionados