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:
- Os círculos internos contêm as políticas de negócio de mais alto nível.
- Os círculos externos contêm os mecanismos técnicos de entrega e infraestrutura.
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:
- O círculo interno define uma abstração (uma interface ou contrato) dizendo o que precisa:
UsuarioRepository. - O círculo externo implementa essa interface:
SqlAlchemyUsuarioRepositoryouFirestoreUsuarioRepository. - Na inicialização do sistema, injetamos a implementação concreta no caso de uso.
2. As Quatro Camadas da Arquitetura
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
Basedo SQLAlchemy, sem decorators de Firestore, etc.).
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).
- Orquestram o fluxo de dados para cumprir tarefas do sistema (ex:
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.
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. |