Wilberhg's blog

Guia Prático: Clean Architecture em Python

A Clean Architecture (Arquitetura Limpa), concebida por Robert C. Martin ("Uncle Bob"), é um modelo de desenho de software cujo objetivo central é isolar as regras de negócio de detalhes técnicos e externos — como frameworks web, bibliotecas de terceiros, drivers de banco de dados e interfaces de usuário.

O princípio orientador é simples: o núcleo da sua aplicação não deve saber qual banco de dados você usa, nem se as requisições chegam via REST API, fila RabbitMQ ou script de linha de comando (CLI).


1. Princípios Fundamentais

1.1. A Regra de Dependência (Dependency Rule)

No diagrama clássico de círculos concêntricos da Clean Architecture:

Frameworks \& Drivers⟶Interface Adapters⟶Use Cases⟶Entities

A Regra de Ouro: O código nos círculos internos nunca deve saber nada sobre as funções, classes ou módulos declarados nos círculos externos. As dependências no código-fonte apontam estritamente de fora para dentro.

1.2. Inversão de Dependência (DIP)

Se um caso de uso precisa salvar dados em um banco, mas não pode importar o banco de dados (que fica no círculo mais externo), como resolver esse paradoxo?

Resolvemos através da Inversão de Dependência:

  1. O círculo interno define uma abstração (uma interface ou contrato) dizendo o que precisa: UsuarioRepository.
  2. O círculo externo implementa essa interface: SqlAlchemyUsuarioRepository ou FirestoreUsuarioRepository.
  3. Na inicialização do sistema, injetamos a implementação concreta no caso de uso.

2. As Quatro Camadas da Arquitetura

  1. Entidades (Enterprise Business Rules):

    • Regras de negócio puras e corporativas.
    • São objetos de domínio independentes de qualquer aplicação específica.
    • Não possuem dependência de ORM (sem Base do SQLAlchemy, sem decorators de Firestore, etc.).
  2. Casos de Uso (Application Business Rules):

    • Orquestram o fluxo de dados para cumprir tarefas do sistema (ex: CadastrarUsuario, ProcessarPagamento).
    • Definem interfaces (portas de entrada e saída) e DTOs (Data Transfer Objects).
  3. Adaptadores de Interface (Interface Adapters):

    • Traduzem dados dos formatos convenientes para o caso de uso para formatos convenientes para entidades externas (e vice-versa).
    • Inclui: Controladores (Controllers), Apresentadores (Presenters), Gateways e Implementações de Repositórios.
  4. Frameworks & Drivers (Frameworks & Devices):

    • A camada mais externa, onde ficam as ferramentas: FastAPI, Flask, SQLAlchemy Engine, Google Cloud SDK, drivers de banco, etc.

3. Estrutura de Diretórios Recomendada

Uma estrutura modular e idiomática em Python:

src/
└── meu_projeto/
    ├── domain/               # Camada 1: Entidades e exceções de negócio
    │   ├── __init__.py
    │   ├── entities.py
    │   └── exceptions.py
    │
    ├── application/          # Camada 2: Casos de uso e portas (interfaces)
    │   ├── __init__.py
    │   ├── dtos.py
    │   ├── use_cases.py
    │   └── ports/
    │       └── repositories.py
    │
    ├── adapters/             # Camada 3: Adaptadores de entrada e saída
    │   ├── __init__.py
    │   ├── controllers/
    │   └── repositories/
    │       ├── memory.py
    │       └── sqlalchemy_repo.py
    │
    └── infrastructure/       # Camada 4: Frameworks, DB engines, configs
        ├── __init__.py
        ├── database.py
        └── web/
            └── api.py

4. Implementação Passo a Passo em Python

Vamos construir um caso de uso real: Criação de um Usuário com validações de regra de negócio e persistência desacoplada.

Passo 1: Entidade de Domínio (domain/entities.py)

A entidade é puro código Python (dataclass), sem acoplamento externo.

from dataclasses import dataclass
from datetime import datetime
import re


@dataclass
class Usuario:
    id: str
    nome: str
    email: str
    ativo: bool = True
    criado_em: datetime = None

    def __post_init__(self):
        if not self.criado_em:
            self.criado_em = datetime.utcnow()
        self.validar_email()

    def validar_email(self) -> None:
        padrao = r"^[\w\.-]+@[\w\.-]+\.\w+$"
        if not re.match(padrao, self.email):
            raise ValueError(f"E-mail inválido: {self.email}")

    def desativar(self) -> None:
        self.ativo = False

Passo 2: Portas e DTOs (application/ports e application/dtos)

Definimos o contrato que o caso de uso espera que o mundo externo cumpra, usando typing.Protocol ou abc.ABC.

Contrato de Repositório (application/ports/repositories.py):

from typing import Protocol, Optional
from domain.entities import Usuario


class UsuarioRepository(Protocol):
    def salvar(self, usuario: Usuario) -> None:
        """Persiste um usuário no armazenamento."""
        ...

    def buscar_por_email(self, email: str) -> Optional[Usuario]:
        """Recupera um usuário pelo e-mail informado."""
        ...

DTOs de Entrada e Saída (application/dtos.py):

from dataclasses import dataclass
from datetime import datetime


@dataclass(frozen=True)
class CriarUsuarioInput:
    nome: str
    email: str


@dataclass(frozen=True)
class UsuarioOutput:
    id: str
    nome: str
    email: str
    ativo: bool
    criado_em: datetime

Passo 3: O Caso de Uso (application/use_cases.py)

O caso de uso apenas manipula o domínio e a porta abstrata de repositório:

import uuid
from application.dtos import CriarUsuarioInput, UsuarioOutput
from application.ports.repositories import UsuarioRepository
from domain.entities import Usuario


class CriarUsuarioUseCase:
    def __init__(self, usuario_repository: UsuarioRepository):
        # Injeção de dependência via construtor
        self.usuario_repo = usuario_repository

    def executar(self, dados: CriarUsuarioInput) -> UsuarioOutput:
        # 1. Regra de Negócio da Aplicação: Verificar duplicidade
        usuario_existente = self.usuario_repo.buscar_por_email(dados.email)
        if usuario_existente:
            raise ValueError("Já existe um usuário cadastrado com este e-mail.")

        # 2. Criar a entidade do domínio (validações do domínio ocorrem aqui)
        novo_usuario = Usuario(
            id=str(uuid.uuid4()),
            nome=dados.nome,
            email=dados.email
        )

        # 3. Persistir via porta abstrata
        self.usuario_repo.salvar(novo_usuario)

        # 4. Retornar DTO de saída isolando a entidade interna
        return UsuarioOutput(
            id=novo_usuario.id,
            nome=novo_usuario.nome,
            email=novo_usuario.email,
            ativo=novo_usuario.ativo,
            criado_em=novo_usuario.criado_em
        )

Passo 4: Implementando Adaptadores (adapters/repositories)

Aqui implementamos a infraestrutura concreta. Podemos ter múltiplas implementações intercambiáveis sem mudar uma linha sequer do UseCase.

Implementação 1: Em Memória (útil para testes rápidos)

from typing import Optional, Dict
from application.ports.repositories import UsuarioRepository
from domain.entities import Usuario


class InMemoryUsuarioRepository(UsuarioRepository):
    def __init__(self):
        self._dados: Dict[str, Usuario] = {}

    def salvar(self, usuario: Usuario) -> None:
        self._dados[usuario.id] = usuario

    def buscar_por_email(self, email: str) -> Optional[Usuario]:
        for usuario in self._dados.values():
            if usuario.email == email:
                return usuario
        return None

Implementação 2: Com SQLAlchemy

from typing import Optional
from sqlalchemy.orm import Session
from application.ports.repositories import UsuarioRepository
from domain.entities import Usuario
# Supondo uma tabela de banco mapeada em infrastructure/database.py:
# from infrastructure.database import UsuarioModel


class SqlAlchemyUsuarioRepository(UsuarioRepository):
    def __init__(self, session: Session):
        self.session = session

    def salvar(self, usuario: Usuario) -> None:
        # Converte a Entidade de Domínio em Model de Banco
        # model = UsuarioModel(id=usuario.id, nome=usuario.nome, email=usuario.email, ...)
        # self.session.add(model)
        # self.session.commit()
        pass

    def buscar_por_email(self, email: str) -> Optional[Usuario]:
        # Busca no banco e reconstrói a Entidade de Domínio pura
        # row = self.session.query(UsuarioModel).filter_by(email=email).first()
        # if not row: return None
        # return Usuario(id=row.id, nome=row.nome, email=row.email, ...)
        pass

Passo 5: Compondo o Sistema (Main / Dependency Injection)

O ponto de entrada da aplicação (seja uma rota FastAPI, um comando CLI ou um script) monta o grafo de objetos conectando as peças:

from adapters.repositories.memory import InMemoryUsuarioRepository
from application.dtos import CriarUsuarioInput
from application.use_cases import CriarUsuarioUseCase


def main():
    # 1. Instancia a infraestrutura concreta
    repo = InMemoryUsuarioRepository()

    # 2. Injeta a infraestrutura no caso de uso
    use_case = CriarUsuarioUseCase(usuario_repository=repo)

    # 3. Executa o fluxo através de DTOs
    payload = CriarUsuarioInput(nome="Wilber Godoy", email="wilber@exemplo.com")

    try:
        resultado = use_case.executar(payload)
        print("Usuário criado com sucesso na arquitetura limpa:")
        print(f"ID: {resultado.id} | Nome: {resultado.nome} | E-mail: {resultado.email}")
    except ValueError as err:
        print(f"Falha de validação: {err}")


if __name__ == "__main__":
    main()

5. Vantagens e Cuidados

Vantagens Cuidados
Independência de Frameworks: Trocar de Flask para FastAPI ou SQLAlchemy para Firestore não altera a lógica de negócios. Custo inicial de código: Criação de mais arquivos, interfaces e DTOs (boilerplate).
Testabilidade Extrema: É possível testar 100% dos casos de uso sem precisar de banco de dados ou rede. Risco de sobre-engenharia (Overengineering): Projetos muito simples (CRUDs triviais) podem não justificar tantas camadas.
Manutenibilidade a longo prazo: Regras corporativas ficam centralizadas e protegidas. Curva de aprendizado: Exige disciplina da equipe para não violar a Regra de Dependência.