Clean Architecture em Python: Estruturando Projetos para Escalar
A Clean Architecture, conceito popularizado por Robert C. Martin, é um padrão de design que coloca a lógica de negócio no centro da aplicação, isolando-a de detalhes técnicos como frameworks, bancos de dados e interfaces de usuário. Em Python, aplicar esses princípios significa organizar seu projeto em camadas concêntricas onde cada uma tem responsabilidades bem definidas e depende apenas das camadas internas.
Quando você estrutura um projeto seguindo Clean Architecture, ganha capacidade de manutenção, testabilidade e flexibilidade para trocar tecnologias sem afetar o núcleo da aplicação. Um projeto que começa pequeno pode crescer significativamente, e uma base sólida arquitetural economiza horas de refatoração futura. Neste artigo, vamos explorar como implementar esses conceitos de forma prática em Python.
As Camadas da Clean Architecture
Entendendo a Estrutura em Camadas
Clean Architecture é frequentemente representada como círculos concêntricos. De fora para dentro, temos: Frameworks & Drivers (camada mais externa), Interface Adapters, Application Business Rules e Entity Business Rules (camada mais interna). A regra fundamental é que as dependências sempre apontam para dentro — a camada mais interna nunca conhece a mais externa.
Em um projeto Python real, estruturamos isso em pacotes e módulos. A camada mais interna contém as entidades (modelos de domínio puros), seguida pela camada de casos de uso (application services), depois os adaptadores (controllers, presenters) e por fim os frameworks e detalhes técnicos.
Estrutura de Diretórios Recomendada
meu_projeto/
├── src/
│ ├── dominio/ # Camada de negócio puro
│ │ ├── entidades.py
│ │ └── value_objects.py
│ ├── casos_uso/ # Regras de negócio da aplicação
│ │ ├── criar_usuario.py
│ │ └── autenticar_usuario.py
│ ├── adaptadores/ # Interface Adapters
│ │ ├── controllers/
│ │ ├── presenters/
│ │ └── gateways/
│ ├── frameworks/ # Detalhes técnicos (Flask, BD, etc)
│ │ ├── web/
│ │ ├── persistencia/
│ │ └── config.py
│ └── __init__.py
├── tests/
├── requirements.txt
└── README.md
Implementando a Camada de Domínio
Entidades e Value Objects
A camada de domínio contém as entidades — objetos que representam conceitos do seu negócio — e value objects — objetos imutáveis que descrevem características. Eles não sabem nada sobre banco de dados, HTTP ou qualquer detalhe técnico. São puros.
Uma entidade tem identidade única e ciclo de vida. Um value object não tem identidade; dois value objects com os mesmos atributos são equivalentes. Vamos ver um exemplo prático:
# src/dominio/entidades.py
from datetime import datetime
from typing import Optional
class Usuario:
"""Entidade de domínio que representa um usuário."""
def __init__(
self,
id: str,
nome: str,
email: str,
senha_hash: str,
data_criacao: Optional[datetime] = None
):
self.id = id
self.nome = nome
self.email = email
self.senha_hash = senha_hash
self.data_criacao = data_criacao or datetime.now()
self.ativo = True
def desativar(self) -> None:
"""Ação de negócio: desativar usuário."""
self.ativo = False
def atualizar_nome(self, novo_nome: str) -> None:
"""Ação de negócio: atualizar nome."""
if not novo_nome or len(novo_nome) < 3:
raise ValueError("Nome deve ter pelo menos 3 caracteres")
self.nome = novo_nome
# src/dominio/value_objects.py
from dataclasses import dataclass
import re
@dataclass(frozen=True)
class Email:
"""Value object imutável para email."""
endereco: str
def __post_init__(self):
if not self._validar():
raise ValueError(f"Email inválido: {self.endereco}")
def _validar(self) -> bool:
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return re.match(pattern, self.endereco) is not None
def __str__(self) -> str:
return self.endereco
Repare que a entidade Usuario contém lógica de negócio (validar nome, desativar). O value object Email encapsula a validação de email. Nenhum deles importa qualquer framework ou detalhe técnico.
Casos de Uso e Portas
Definindo Casos de Uso (Interactors)
Um caso de uso representa uma funcionalidade de negócio que a aplicação executa. Ele orquestra entidades e repositories (que são abstrações, não implementações). Os casos de uso devem ser independentes de frameworks e tecnologias específicas.
# src/casos_uso/criar_usuario.py
from abc import ABC, abstractmethod
from src.dominio.entidades import Usuario
from src.dominio.value_objects import Email
class RepositorioUsuario(ABC):
"""Porto (interface) para persistência de usuários."""
@abstractmethod
def salvar(self, usuario: Usuario) -> None:
pass
@abstractmethod
def obter_por_email(self, email: str) -> Usuario | None:
pass
class GeradorIdUsuario(ABC):
"""Porto para gerar IDs."""
@abstractmethod
def gerar(self) -> str:
pass
class HashearSenha(ABC):
"""Porto para hashing de senhas."""
@abstractmethod
def hashear(self, senha: str) -> str:
pass
class CriarUsuarioUseCase:
"""Caso de uso: criar um novo usuário."""
def __init__(
self,
repositorio: RepositorioUsuario,
gerador_id: GeradorIdUsuario,
hasheador: HashearSenha
):
self.repositorio = repositorio
self.gerador_id = gerador_id
self.hasheador = hasheador
def executar(self, nome: str, email: str, senha: str) -> Usuario:
"""
Executa a lógica de negócio para criar um usuário.
Exceções levantadas aqui são exceções de negócio,
não técnicas.
"""
# Validar email (value object faz isso)
email_validado = Email(email)
# Verificar se usuário já existe
usuario_existente = self.repositorio.obter_por_email(email)
if usuario_existente:
raise ValueError(f"Usuário com email {email} já existe")
# Criar usuário
novo_usuario = Usuario(
id=self.gerador_id.gerar(),
nome=nome,
email=str(email_validado),
senha_hash=self.hasheador.hashear(senha)
)
# Persistir
self.repositorio.salvar(novo_usuario)
return novo_usuario
Observe que o caso de uso não sabe como a senha é hashada, como o ID é gerado ou como o usuário é armazenado. Ele depende de abstrações (portos), permitindo diferentes implementações.
Adaptadores e Frameworks
Implementando os Adaptadores
Os adaptadores convertem dados entre o mundo externo (web, CLI, mensagens) e o mundo interno (casos de uso). Aqui implementamos as portas definidas nos casos de uso.
# src/adaptadores/gateways/repositorio_usuario_sqlite.py
import sqlite3
import json
from src.casos_uso.criar_usuario import RepositorioUsuario
from src.dominio.entidades import Usuario
from datetime import datetime
class RepositorioUsuarioSQLite(RepositorioUsuario):
"""Implementação de persistência com SQLite."""
def __init__(self, caminho_db: str):
self.caminho_db = caminho_db
self._inicializar_db()
def _inicializar_db(self):
with sqlite3.connect(self.caminho_db) as conn:
conn.execute('''
CREATE TABLE IF NOT EXISTS usuarios (
id TEXT PRIMARY KEY,
nome TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
senha_hash TEXT NOT NULL,
data_criacao TEXT NOT NULL,
ativo INTEGER NOT NULL
)
''')
conn.commit()
def salvar(self, usuario: Usuario) -> None:
with sqlite3.connect(self.caminho_db) as conn:
conn.execute('''
INSERT OR REPLACE INTO usuarios
(id, nome, email, senha_hash, data_criacao, ativo)
VALUES (?, ?, ?, ?, ?, ?)
''', (
usuario.id,
usuario.nome,
usuario.email,
usuario.senha_hash,
usuario.data_criacao.isoformat(),
1 if usuario.ativo else 0
))
conn.commit()
def obter_por_email(self, email: str) -> Usuario | None:
with sqlite3.connect(self.caminho_db) as conn:
cursor = conn.execute(
'SELECT id, nome, email, senha_hash, data_criacao FROM usuarios WHERE email = ?',
(email,)
)
linha = cursor.fetchone()
if not linha:
return None
return Usuario(
id=linha[0],
nome=linha[1],
email=linha[2],
senha_hash=linha[3],
data_criacao=datetime.fromisoformat(linha[4])
)
# src/adaptadores/gateways/geradores.py
import uuid
from src.casos_uso.criar_usuario import GeradorIdUsuario, HashearSenha
import hashlib
class GeradorIdUUID(GeradorIdUsuario):
"""Gera IDs usando UUID."""
def gerar(self) -> str:
return str(uuid.uuid4())
class HashearSenhaSimples(HashearSenha):
"""Hasheador de senhas com SHA-256 (use bcrypt em produção!)."""
def hashear(self, senha: str) -> str:
return hashlib.sha256(senha.encode()).hexdigest()
# src/adaptadores/controllers/usuario_controller.py
from src.casos_uso.criar_usuario import CriarUsuarioUseCase
from src.adaptadores.presenters.usuario_presenter import UsuarioPresenter
class UsuarioController:
"""Controller para requisições de usuário (via HTTP, CLI, etc)."""
def __init__(self, caso_uso: CriarUsuarioUseCase, presenter: UsuarioPresenter):
self.caso_uso = caso_uso
self.presenter = presenter
def criar_usuario(self, dados: dict) -> dict:
"""
Recebe dados do cliente (desserializados de JSON, por exemplo)
e retorna resposta formatada.
"""
try:
usuario = self.caso_uso.executar(
nome=dados['nome'],
email=dados['email'],
senha=dados['senha']
)
return self.presenter.usuario_criado(usuario)
except ValueError as e:
return self.presenter.erro_validacao(str(e))
Presenter e Formatação de Resposta
# src/adaptadores/presenters/usuario_presenter.py
from src.dominio.entidades import Usuario
from datetime import datetime
class UsuarioPresenter:
"""Presenter que formata dados de domínio para resposta HTTP/API."""
def usuario_criado(self, usuario: Usuario) -> dict:
return {
'status': 'sucesso',
'dados': {
'id': usuario.id,
'nome': usuario.nome,
'email': usuario.email,
'data_criacao': usuario.data_criacao.isoformat(),
'ativo': usuario.ativo
}
}
def erro_validacao(self, mensagem: str) -> dict:
return {
'status': 'erro',
'mensagem': mensagem
}
Montando Tudo com Dependency Injection
Factory e Container de Injeção
# src/frameworks/container.py
from src.casos_uso.criar_usuario import (
CriarUsuarioUseCase,
RepositorioUsuario,
GeradorIdUsuario,
HashearSenha
)
from src.adaptadores.gateways.repositorio_usuario_sqlite import RepositorioUsuarioSQLite
from src.adaptadores.gateways.geradores import GeradorIdUUID, HashearSenhaSimples
from src.adaptadores.controllers.usuario_controller import UsuarioController
from src.adaptadores.presenters.usuario_presenter import UsuarioPresenter
class Container:
"""Container de injeção de dependência."""
def __init__(self, caminho_db: str = 'app.db'):
self._repositorio_usuario: RepositorioUsuario = RepositorioUsuarioSQLite(caminho_db)
self._gerador_id: GeradorIdUsuario = GeradorIdUUID()
self._hasheador: HashearSenha = HashearSenhaSimples()
def criar_usuario_use_case(self) -> CriarUsuarioUseCase:
return CriarUsuarioUseCase(
repositorio=self._repositorio_usuario,
gerador_id=self._gerador_id,
hasheador=self._hasheador
)
def usuario_controller(self) -> UsuarioController:
return UsuarioController(
caso_uso=self.criar_usuario_use_case(),
presenter=UsuarioPresenter()
)
Exemplo de Uso com Flask
# src/frameworks/web/app.py
from flask import Flask, request, jsonify
from src.frameworks.container import Container
app = Flask(__name__)
container = Container()
@app.route('/usuarios', methods=['POST'])
def criar_usuario():
dados = request.get_json()
controller = container.usuario_controller()
resultado = controller.criar_usuario(dados)
status_code = 201 if resultado['status'] == 'sucesso' else 400
return jsonify(resultado), status_code
if __name__ == '__main__':
app.run(debug=True)
Testando com Clean Architecture
Testes Unitários sem Dependências Externas
# tests/test_criar_usuario_use_case.py
import unittest
from unittest.mock import Mock
from src.casos_uso.criar_usuario import CriarUsuarioUseCase
from src.dominio.entidades import Usuario
class MockRepositorio:
def __init__(self):
self.usuarios = {}
def salvar(self, usuario: Usuario) -> None:
self.usuarios[usuario.id] = usuario
def obter_por_email(self, email: str) -> Usuario | None:
for usuario in self.usuarios.values():
if usuario.email == email:
return usuario
return None
class MockGerador:
def gerar(self) -> str:
return "id_teste_123"
class MockHasheador:
def hashear(self, senha: str) -> str:
return f"hash({senha})"
class TestCriarUsuarioUseCase(unittest.TestCase):
def setUp(self):
self.repositorio = MockRepositorio()
self.gerador = MockGerador()
self.hasheador = MockHasheador()
self.caso_uso = CriarUsuarioUseCase(
self.repositorio,
self.gerador,
self.hasheador
)
def test_criar_usuario_com_sucesso(self):
usuario = self.caso_uso.executar(
nome="João Silva",
email="joao@example.com",
senha="senha123"
)
self.assertEqual(usuario.nome, "João Silva")
self.assertEqual(usuario.email, "joao@example.com")
self.assertTrue(usuario.ativo)
def test_email_invalido_levanta_excecao(self):
with self.assertRaises(ValueError):
self.caso_uso.executar(
nome="João Silva",
email="email_invalido",
senha="senha123"
)
def test_usuario_duplicado_levanta_excecao(self):
self.caso_uso.executar(
nome="João Silva",
email="joao@example.com",
senha="senha123"
)
with self.assertRaises(ValueError):
self.caso_uso.executar(
nome="Outro João",
email="joao@example.com",
senha="outra_senha"
)
if __name__ == '__main__':
unittest.main()
Veja que os testes não usam banco de dados real nem qualquer framework. São testes de lógica de negócio pura.
Conclusão
Clean Architecture em Python oferece três benefícios fundamentais que você sentirá imediatamente: isolamento de lógica de negócio torna seus testes simples e rápidos — você testa regras de negócio sem infra; flexibilidade para trocar tecnologias permite mudar de SQLite para PostgreSQL ou de Flask para FastAPI sem tocar em uma única linha de caso de uso; comunicação clara entre camadas via portos e adaptadores mantém o projeto organizado mesmo quando cresce de 10 para 100 mil linhas.
Não é necessário ser obsessivo — alguns projetos simples não precisam dessa estrutura completa. Mas quando seu projeto escala, essa arquitetura economiza refatorações massivas. Comece aplicando em um novo projeto e sinta como a organização facilita manutenção, testes e comunicação com seu time.