Python

Clean Architecture em Python: Estruturando Projetos para Escalar na Prática

15 min de leitura

Clean Architecture em Python: Estruturando Projetos para Escalar na Prática

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

<h2>Clean Architecture em Python: Estruturando Projetos para Escalar</h2>

<p>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.</p>

<p>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.</p>

<h2>As Camadas da Clean Architecture</h2>

<h3>Entendendo a Estrutura em Camadas</h3>

<p>Clean Architecture é frequentemente representada como círculos concêntricos. De fora para dentro, temos: Frameworks &amp; Drivers (camada mais externa), Interface Adapters, Application Business Rules e Entity Business Rules (camada mais interna). A regra fundamental é que <strong>as dependências sempre apontam para dentro</strong> — a camada mais interna nunca conhece a mais externa.</p>

<p>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.</p>

<h3>Estrutura de Diretórios Recomendada</h3>

<pre><code>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</code></pre>

<h2>Implementando a Camada de Domínio</h2>

<h3>Entidades e Value Objects</h3>

<p>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.</p>

<p>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:</p>

<pre><code class="language-python"># src/dominio/entidades.py

from datetime import datetime

from typing import Optional

class Usuario:

&quot;&quot;&quot;Entidade de domínio que representa um usuário.&quot;&quot;&quot;

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) -&gt; None:

&quot;&quot;&quot;Ação de negócio: desativar usuário.&quot;&quot;&quot;

self.ativo = False

def atualizar_nome(self, novo_nome: str) -&gt; None:

&quot;&quot;&quot;Ação de negócio: atualizar nome.&quot;&quot;&quot;

if not novo_nome or len(novo_nome) &lt; 3:

raise ValueError(&quot;Nome deve ter pelo menos 3 caracteres&quot;)

self.nome = novo_nome</code></pre>

<pre><code class="language-python"># src/dominio/value_objects.py

from dataclasses import dataclass

import re

@dataclass(frozen=True)

class Email:

&quot;&quot;&quot;Value object imutável para email.&quot;&quot;&quot;

endereco: str

def __post_init__(self):

if not self._validar():

raise ValueError(f&quot;Email inválido: {self.endereco}&quot;)

def _validar(self) -&gt; bool:

pattern = r&#039;^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$&#039;

return re.match(pattern, self.endereco) is not None

def __str__(self) -&gt; str:

return self.endereco</code></pre>

<p>Repare que a entidade <code>Usuario</code> contém lógica de negócio (validar nome, desativar). O value object <code>Email</code> encapsula a validação de email. Nenhum deles importa qualquer framework ou detalhe técnico.</p>

<h2>Casos de Uso e Portas</h2>

<h3>Definindo Casos de Uso (Interactors)</h3>

<p>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.</p>

<pre><code class="language-python"># 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):

&quot;&quot;&quot;Porto (interface) para persistência de usuários.&quot;&quot;&quot;

@abstractmethod

def salvar(self, usuario: Usuario) -&gt; None:

pass

@abstractmethod

def obter_por_email(self, email: str) -&gt; Usuario | None:

pass

class GeradorIdUsuario(ABC):

&quot;&quot;&quot;Porto para gerar IDs.&quot;&quot;&quot;

@abstractmethod

def gerar(self) -&gt; str:

pass

class HashearSenha(ABC):

&quot;&quot;&quot;Porto para hashing de senhas.&quot;&quot;&quot;

@abstractmethod

def hashear(self, senha: str) -&gt; str:

pass

class CriarUsuarioUseCase:

&quot;&quot;&quot;Caso de uso: criar um novo usuário.&quot;&quot;&quot;

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) -&gt; Usuario:

&quot;&quot;&quot;

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.

&quot;&quot;&quot;

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&quot;Usuário com email {email} já existe&quot;)

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</code></pre>

<p>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.</p>

<h2>Adaptadores e Frameworks</h2>

<h3>Implementando os Adaptadores</h3>

<p>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.</p>

<pre><code class="language-python"># 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):

&quot;&quot;&quot;Implementação de persistência com SQLite.&quot;&quot;&quot;

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(&#039;&#039;&#039;

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

)

&#039;&#039;&#039;)

conn.commit()

def salvar(self, usuario: Usuario) -&gt; None:

with sqlite3.connect(self.caminho_db) as conn:

conn.execute(&#039;&#039;&#039;

INSERT OR REPLACE INTO usuarios

(id, nome, email, senha_hash, data_criacao, ativo)

VALUES (?, ?, ?, ?, ?, ?)

&#039;&#039;&#039;, (

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) -&gt; Usuario | None:

with sqlite3.connect(self.caminho_db) as conn:

cursor = conn.execute(

&#039;SELECT id, nome, email, senha_hash, data_criacao FROM usuarios WHERE email = ?&#039;,

(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])

)</code></pre>

<pre><code class="language-python"># src/adaptadores/gateways/geradores.py

import uuid

from src.casos_uso.criar_usuario import GeradorIdUsuario, HashearSenha

import hashlib

class GeradorIdUUID(GeradorIdUsuario):

&quot;&quot;&quot;Gera IDs usando UUID.&quot;&quot;&quot;

def gerar(self) -&gt; str:

return str(uuid.uuid4())

class HashearSenhaSimples(HashearSenha):

&quot;&quot;&quot;Hasheador de senhas com SHA-256 (use bcrypt em produção!).&quot;&quot;&quot;

def hashear(self, senha: str) -&gt; str:

return hashlib.sha256(senha.encode()).hexdigest()</code></pre>

<pre><code class="language-python"># src/adaptadores/controllers/usuario_controller.py

from src.casos_uso.criar_usuario import CriarUsuarioUseCase

from src.adaptadores.presenters.usuario_presenter import UsuarioPresenter

class UsuarioController:

&quot;&quot;&quot;Controller para requisições de usuário (via HTTP, CLI, etc).&quot;&quot;&quot;

def __init__(self, caso_uso: CriarUsuarioUseCase, presenter: UsuarioPresenter):

self.caso_uso = caso_uso

self.presenter = presenter

def criar_usuario(self, dados: dict) -&gt; dict:

&quot;&quot;&quot;

Recebe dados do cliente (desserializados de JSON, por exemplo)

e retorna resposta formatada.

&quot;&quot;&quot;

try:

usuario = self.caso_uso.executar(

nome=dados[&#039;nome&#039;],

email=dados[&#039;email&#039;],

senha=dados[&#039;senha&#039;]

)

return self.presenter.usuario_criado(usuario)

except ValueError as e:

return self.presenter.erro_validacao(str(e))</code></pre>

<h3>Presenter e Formatação de Resposta</h3>

<pre><code class="language-python"># src/adaptadores/presenters/usuario_presenter.py

from src.dominio.entidades import Usuario

from datetime import datetime

class UsuarioPresenter:

&quot;&quot;&quot;Presenter que formata dados de domínio para resposta HTTP/API.&quot;&quot;&quot;

def usuario_criado(self, usuario: Usuario) -&gt; dict:

return {

&#039;status&#039;: &#039;sucesso&#039;,

&#039;dados&#039;: {

&#039;id&#039;: usuario.id,

&#039;nome&#039;: usuario.nome,

&#039;email&#039;: usuario.email,

&#039;data_criacao&#039;: usuario.data_criacao.isoformat(),

&#039;ativo&#039;: usuario.ativo

}

}

def erro_validacao(self, mensagem: str) -&gt; dict:

return {

&#039;status&#039;: &#039;erro&#039;,

&#039;mensagem&#039;: mensagem

}</code></pre>

<h2>Montando Tudo com Dependency Injection</h2>

<h3>Factory e Container de Injeção</h3>

<pre><code class="language-python"># 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:

&quot;&quot;&quot;Container de injeção de dependência.&quot;&quot;&quot;

def __init__(self, caminho_db: str = &#039;app.db&#039;):

self._repositorio_usuario: RepositorioUsuario = RepositorioUsuarioSQLite(caminho_db)

self._gerador_id: GeradorIdUsuario = GeradorIdUUID()

self._hasheador: HashearSenha = HashearSenhaSimples()

def criar_usuario_use_case(self) -&gt; CriarUsuarioUseCase:

return CriarUsuarioUseCase(

repositorio=self._repositorio_usuario,

gerador_id=self._gerador_id,

hasheador=self._hasheador

)

def usuario_controller(self) -&gt; UsuarioController:

return UsuarioController(

caso_uso=self.criar_usuario_use_case(),

presenter=UsuarioPresenter()

)</code></pre>

<h3>Exemplo de Uso com Flask</h3>

<pre><code class="language-python"># src/frameworks/web/app.py

from flask import Flask, request, jsonify

from src.frameworks.container import Container

app = Flask(__name__)

container = Container()

@app.route(&#039;/usuarios&#039;, methods=[&#039;POST&#039;])

def criar_usuario():

dados = request.get_json()

controller = container.usuario_controller()

resultado = controller.criar_usuario(dados)

status_code = 201 if resultado[&#039;status&#039;] == &#039;sucesso&#039; else 400

return jsonify(resultado), status_code

if __name__ == &#039;__main__&#039;:

app.run(debug=True)</code></pre>

<h2>Testando com Clean Architecture</h2>

<h3>Testes Unitários sem Dependências Externas</h3>

<pre><code class="language-python"># 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) -&gt; None:

self.usuarios[usuario.id] = usuario

def obter_por_email(self, email: str) -&gt; Usuario | None:

for usuario in self.usuarios.values():

if usuario.email == email:

return usuario

return None

class MockGerador:

def gerar(self) -&gt; str:

return &quot;id_teste_123&quot;

class MockHasheador:

def hashear(self, senha: str) -&gt; str:

return f&quot;hash({senha})&quot;

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=&quot;João Silva&quot;,

email=&quot;joao@example.com&quot;,

senha=&quot;senha123&quot;

)

self.assertEqual(usuario.nome, &quot;João Silva&quot;)

self.assertEqual(usuario.email, &quot;joao@example.com&quot;)

self.assertTrue(usuario.ativo)

def test_email_invalido_levanta_excecao(self):

with self.assertRaises(ValueError):

self.caso_uso.executar(

nome=&quot;João Silva&quot;,

email=&quot;email_invalido&quot;,

senha=&quot;senha123&quot;

)

def test_usuario_duplicado_levanta_excecao(self):

self.caso_uso.executar(

nome=&quot;João Silva&quot;,

email=&quot;joao@example.com&quot;,

senha=&quot;senha123&quot;

)

with self.assertRaises(ValueError):

self.caso_uso.executar(

nome=&quot;Outro João&quot;,

email=&quot;joao@example.com&quot;,

senha=&quot;outra_senha&quot;

)

if __name__ == &#039;__main__&#039;:

unittest.main()</code></pre>

<p>Veja que os testes não usam banco de dados real nem qualquer framework. São testes de lógica de negócio pura.</p>

<h2>Conclusão</h2>

<p>Clean Architecture em Python oferece três benefícios fundamentais que você sentirá imediatamente: <strong>isolamento de lógica de negócio</strong> torna seus testes simples e rápidos — você testa regras de negócio sem infra; <strong>flexibilidade para trocar tecnologias</strong> permite mudar de SQLite para PostgreSQL ou de Flask para FastAPI sem tocar em uma única linha de caso de uso; <strong>comunicação clara entre camadas</strong> via portos e adaptadores mantém o projeto organizado mesmo quando cresce de 10 para 100 mil linhas.</p>

<p>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.</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 - Robert C. Martin</a></li>

<li><a href="https://docs.python.org/3/library/abc.html" target="_blank" rel="noopener noreferrer">Documentação Python: abc (Abstract Base Classes)</a></li>

<li><a href="https://alistair.cockburn.us/hexagonal-architecture/" target="_blank" rel="noopener noreferrer">Hexagonal Architecture (Ports &amp; Adapters) - Alistair Cockburn</a></li>

<li><a href="https://www.oreilly.com/library/view/domain-driven-design-tackling/9780321125675/" target="_blank" rel="noopener noreferrer">Domain-Driven Design - Eric Evans</a></li>

<li><a href="https://realpython.com/dependency-injection-python/" target="_blank" rel="noopener noreferrer">Dependency Injection in Python - Real Python</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Python

Django em Python: MVT, ORM, Admin e Estrutura de Projetos: Do Básico ao Avançado
Django em Python: MVT, ORM, Admin e Estrutura de Projetos: Do Básico ao Avançado

Entendendo o MVT: A Arquitetura do Django Django segue um padrão arquitetural...

Guia Completo de Context Managers em Python: with, __enter__, __exit__ e contextlib
Guia Completo de Context Managers em Python: with, __enter__, __exit__ e contextlib

O que são Context Managers? Context managers são um padrão de design em Pytho...

O que Todo Dev Deve Saber sobre Alembic em Python: Migrations Versionadas com SQLAlchemy
O que Todo Dev Deve Saber sobre Alembic em Python: Migrations Versionadas com SQLAlchemy

O que é Alembic e Por Que Você Precisa Dele Alembic é uma ferramenta de versi...