Em 2012, Robert C. Martin publicou no seu blog um artigo chamado "The Clean Architecture". Não era ideia completamente nova — ele próprio reconhecia dívida a Hexagonal de Cockburn, à Onion Architecture de Palermo, e à Screaming Architecture que havia descrito antes. O que Martin fez foi sintetizar e formalizar: quatro anéis concêntricos, uma regra de dependência absoluta, e uma afirmação central que vai além de organização de código. A afirmação é esta: frameworks, bancos de dados, e interfaces de usuário são detalhes. O sistema pode e deve ser independente deles — e se não é, você pagará o preço toda vez que um desses detalhes mudar.
Os quatro anéis
Clean Architecture é visualizada como círculos concêntricos. Do centro para fora:
Entities (anel mais interno): o núcleo do sistema. Contém as regras de negócio mais gerais — regras que existiriam mesmo sem computador, que mudariam apenas se o negócio fundamental mudar. Em um sistema bancário, as regras de como juros são calculados são entities. Em um e-commerce, as regras de como um desconto é aplicado a um pedido são entities. Martin as chama de "enterprise business rules" — regras que poderiam ser válidas para múltiplas aplicações dentro da mesma empresa.
Use Cases (segundo anel): contém as regras de negócio específicas da aplicação — os casos de uso do sistema. Aqui vive "o usuário pode cancelar um pedido se ainda não foi despachado". Isso é específico desta aplicação, não uma regra universal de negócio. Use Cases orquestram Entities: eles chamam métodos de domain objects, coordenam transações, e dirigem o fluxo de dados para dentro e para fora do anel mais interno. Este anel é o grande diferencial de Clean Architecture em relação à maioria das variações — Use Cases são cidadãos de primeira classe, explicitamente nomeados e localizados.
Interface Adapters (terceiro anel): contém o código que converte dados entre o formato conveniente para Use Cases e Entities e o formato conveniente para o mundo externo. Aqui vivem controllers, presenters, gateways. Um controller HTTP converte a requisição HTTP em uma estrutura que o Use Case entende; o presenter converte o resultado do Use Case em JSON. Um gateway converte dados do banco de dados em Entities.
Frameworks & Drivers (anel mais externo): o maquinário — frameworks web, banco de dados, UI, dispositivos externos. Este anel contém detalhes que devem ficar o mais fora possível. Nenhum código importante do sistema deve viver aqui.
A Dependency Rule
A regra que define Clean Architecture é simples e absoluta: dependências de código-fonte só podem apontar para dentro. Um Use Case pode depender de um Entity — nunca o contrário. Um Controller pode depender de um Use Case — nunca o contrário. Nada dos anéis externos pode ser mencionado pelo nome nos anéis internos.
"Mencionado pelo nome" é proposital. Não é apenas sobre imports — é sobre qualquer forma de acoplamento. Se um Use Case menciona o nome de uma classe de controller, ele passou a depender do controller. Se uma Entity menciona o nome de uma tabela de banco, ela passou a depender do banco. A Dependency Rule é violada mesmo quando não há import explícito, se a dependência é implícita.
Dependências de código-fonte só apontam para dentro. Entities não conhecem Use Cases; Use Cases não conhecem Controllers; Controllers não conhecem Frameworks além do que é estritamente necessário para funcionar.
Cruzando fronteiras de anéis
O problema prático é: como um Controller pode chamar um Use Case se o Controller está num anel externo e o Use Case está num anel interno? A chamada vai de fora para dentro — isso é permitido pela Dependency Rule. O problema é a direção inversa: quando o Use Case precisa retornar dados ao Controller, ele não pode depender do Controller para isso.
A solução é o princípio de inversão de dependência. O Use Case define uma interface de output (um output port, no vocabulário Hexagonal). O Controller implementa essa interface e a fornece ao Use Case durante a construção. O Use Case chama a interface sem saber que está chamando o Controller — ele chama algo que satisfaz o contrato, e o contrato está definido no anel do Use Case.
Esse mecanismo é idêntico ao que Hexagonal chama de "driven port". A diferença terminológica é pequena; a ideia é a mesma.
Clean Architecture vs Hexagonal — a diferença real
Os dois estilos resolvem o mesmo problema fundamental (isolar domínio de tecnologia) com a mesma mecânica (interfaces definidas no domínio, implementadas fora). As diferenças são de ênfase e de vocabulário:
Clean Architecture nomeia explicitamente o anel de Use Cases como separado das Entities. Hexagonal tende a tratar ambos como "domínio" sem distinguir os dois. Para sistemas simples, isso não importa muito. Para sistemas grandes com Use Cases complexos que orquestram muitas Entities, a separação explícita ajuda a manter Use Cases enxutos e Entities focadas em invariantes.
Hexagonal usa a metáfora de driving/driven adapters, que é mais visual para pensar em integrações externas. Clean Architecture usa a metáfora de anéis concêntricos, que é mais visual para pensar em hierarquia de abstração. Para código real, a diferença é principalmente como você comunica a estrutura para o time.
Implementação: a estrutura de pasta que conta
// Estrutura de projeto C# seguindo Clean Architecture
// Orders.Domain/ (Entities — anel mais interno)
// Entities/Order.cs
// ValueObjects/Money.cs
// Events/OrderPlaced.cs
//
// Orders.Application/ (Use Cases)
// UseCases/PlaceOrder/
// PlaceOrderCommand.cs
// PlaceOrderHandler.cs — orquestra Domain
// IOrderOutputPort.cs — output port (saída para anel externo)
//
// Orders.Adapters/ (Interface Adapters)
// Http/OrdersController.cs — driving adapter
// Presenters/OrderPresenter.cs — implementa IOrderOutputPort
// Persistence/EfOrderGateway.cs
//
// Orders.Infrastructure/ (Frameworks & Drivers)
// Persistence/AppDbContext.cs
// Messaging/RabbitMqPublisher.cs
// Use Case com output port explícito
public interface IPlaceOrderOutputPort // definido em Application (anel 2)
{
void OrderPlaced(OrderId id, Money total);
void PaymentFailed(string reason);
}
public class PlaceOrderHandler
{
private readonly IOrderRepository _orders; // port definido em Domain
private readonly IPaymentGateway _payments; // port definido em Application
private readonly IPlaceOrderOutputPort _output;
public async Task Handle(PlaceOrderCommand cmd)
{
var order = Order.Create(cmd.CustomerId, cmd.Lines); // Entity
var result = await _payments.ChargeAsync(order.ToPaymentRequest());
if (!result.IsSuccess)
{
_output.PaymentFailed(result.Error); // não retorna — notifica
return;
}
order.MarkPaid(result.TransactionId);
await _orders.SaveAsync(order);
_output.OrderPlaced(order.Id, order.Total); // notifica output port
}
}
// Adapter (Controller) implementa IPlaceOrderOutputPort
public class OrdersController : ControllerBase, IPlaceOrderOutputPort
{
private IActionResult? _result;
public void OrderPlaced(OrderId id, Money total)
=> _result = CreatedAtAction(nameof(Get), new { id = id.Value }, new { total });
public void PaymentFailed(string reason)
=> _result = BadRequest(new { error = reason });
[HttpPost]
public async Task<IActionResult> Place(PlaceOrderRequest request)
{
var handler = new PlaceOrderHandler(_orders, _payments, this);
await handler.Handle(request.ToCommand());
return _result!;
}
}
O controller implementa IPlaceOrderOutputPort — inverte a dependência. O Use Case notifica via interface sem saber que está falando com HTTP.
# Estrutura de pacotes seguindo Clean Architecture
# orders/
# domain/ (Entities)
# order.py — aggregate root
# money.py — value object
# application/ (Use Cases)
# use_cases/
# place_order.py — use case
# ports.py — output ports (interfaces)
# adapters/ (Interface Adapters)
# http/
# orders_router.py — driving adapter (FastAPI)
# persistence/
# postgres_repo.py — driven adapter
# infrastructure/ (Frameworks & Drivers)
# db.py — engine SQLAlchemy
# domain/order.py — Entities, sem import de use cases ou adapters
from dataclasses import dataclass, field
from decimal import Decimal
@dataclass
class Order:
id: str
customer_id: str
lines: list = field(default_factory=list)
status: str = "draft"
def confirm(self) -> None:
if not self.lines:
raise ValueError("Pedido vazio")
self.status = "confirmed"
# application/ports.py — output ports (definidos em Application)
from abc import ABC, abstractmethod
class PlaceOrderOutputPort(ABC): # output port
@abstractmethod
def order_placed(self, order_id: str, total: Decimal) -> None: ...
@abstractmethod
def payment_failed(self, reason: str) -> None: ...
class OrderRepository(ABC): # port de persistência
@abstractmethod
async def save(self, order: "Order") -> None: ...
# application/use_cases/place_order.py — Use Case
class PlaceOrderUseCase:
def __init__(
self,
orders: OrderRepository,
gateway: "PaymentGateway",
output: PlaceOrderOutputPort,
):
self._orders = orders
self._gateway = gateway
self._output = output
async def execute(self, cmd: PlaceOrderCommand) -> None:
order = Order.create(cmd.customer_id, cmd.lines)
result = await self._gateway.charge(order.total(), cmd.token)
if not result.success:
self._output.payment_failed(result.error)
return
order.confirm()
await self._orders.save(order)
self._output.order_placed(order.id, order.total())
# adapters/http/orders_router.py — driving adapter, implementa output port
from fastapi import APIRouter
from fastapi.responses import JSONResponse
router = APIRouter()
class OrderHttpPresenter(PlaceOrderOutputPort):
def __init__(self): self.response: JSONResponse | None = None
def order_placed(self, order_id: str, total: Decimal) -> None:
self.response = JSONResponse({"id": order_id, "total": float(total)}, 201)
def payment_failed(self, reason: str) -> None:
self.response = JSONResponse({"error": reason}, 400)
@router.post("/orders")
async def place_order(body: PlaceOrderRequest):
presenter = OrderHttpPresenter()
uc = PlaceOrderUseCase(get_order_repo(), get_payment_gw(), presenter)
await uc.execute(body.to_command())
return presenter.response
OrderHttpPresenter implementa o output port. O Use Case chama self._output.order_placed() sem saber que é HTTP — a Dependency Rule se mantém.
// Estrutura de pacotes
// internal/orders/
// domain/ (Entities)
// order.go
// money.go
// app/ (Use Cases)
// place_order.go
// ports.go — output ports
// adapters/
// http/ (driving adapter)
// postgres/ (driven adapter)
// domain/order.go — Entities
package domain
type Order struct {
ID string
CustomerID string
Lines []OrderLine
Status string
}
func (o *Order) Confirm() error {
if len(o.Lines) == 0 {
return errors.New("pedido vazio")
}
o.Status = "confirmed"
return nil
}
// app/ports.go — output ports definidos em Application
package app
import "context"
type PlaceOrderOutputPort interface {
OrderPlaced(ctx context.Context, orderID string, total float64)
PaymentFailed(ctx context.Context, reason string)
}
type OrderRepository interface {
Save(ctx context.Context, order *domain.Order) error
}
// app/place_order.go — Use Case
package app
type PlaceOrderUseCase struct {
orders OrderRepository
gateway PaymentGateway
output PlaceOrderOutputPort
}
func (uc *PlaceOrderUseCase) Execute(ctx context.Context, cmd PlaceOrderCommand) {
order := domain.NewOrder(cmd.CustomerID, cmd.Lines)
result := uc.gateway.Charge(ctx, order.Total(), cmd.Token)
if !result.Success {
uc.output.PaymentFailed(ctx, result.Reason)
return
}
order.Confirm()
uc.orders.Save(ctx, order)
uc.output.OrderPlaced(ctx, order.ID, order.Total())
}
// adapters/http/orders.go — driving adapter + presenter
package http
type OrdersHandler struct {
orders app.OrderRepository
gateway app.PaymentGateway
}
type orderPresenter struct {
w http.ResponseWriter
code int
body any
}
func (p *orderPresenter) OrderPlaced(_ context.Context, id string, total float64) {
p.code = 201
p.body = map[string]any{"id": id, "total": total}
}
func (p *orderPresenter) PaymentFailed(_ context.Context, reason string) {
p.code = 400
p.body = map[string]string{"error": reason}
}
func (h *OrdersHandler) PlaceOrder(w http.ResponseWriter, r *http.Request) {
var req PlaceOrderRequest
json.NewDecoder(r.Body).Decode(&req)
presenter := &orderPresenter{w: w}
uc := &app.PlaceOrderUseCase{
orders: h.orders,
gateway: h.gateway,
output: presenter,
}
uc.Execute(r.Context(), req.ToCommand())
w.WriteHeader(presenter.code)
json.NewEncoder(w).Encode(presenter.body)
}
orderPresenter implementa PlaceOrderOutputPort. O Use Case nunca vê http.ResponseWriter — a Dependency Rule é preservada em Go sem herança.
Quando o output port complica sem benefício
O padrão de output port (presenter) que Clean Architecture propõe é poderoso, mas pode ser over-engineering em muitos sistemas. Para casos de uso simples onde o resultado é um valor direto (não múltiplos ramos de output), retornar um valor ou lançar exceção é mais simples e igualmente válido.
A regra prática: use output ports quando o Use Case precisa comunicar múltiplos resultados diferentes para quem chamou (como o padrão de Result com casos de sucesso/falha distintos). Para casos de uso com resultado único, retorno direto é mais legível.
A essência de Clean Architecture não está no output port — está na Dependency Rule e na separação de Entities de Use Cases de Adapters. Você pode implementar Clean Architecture sem a formalidade do presenter explícito e ainda ganhar os benefícios centrais do estilo.
Referências para aprofundar
- artigo The Clean Architecture — Robert C. Martin. blog.cleancoder.com, 2012.
- livro Clean Architecture — Robert C. Martin. Prentice Hall, 2017.
- artigo Hexagonal Architecture — Alistair Cockburn. alistair.cockburn.us, 2005.
- artigo The Onion Architecture — Jeffrey Palermo. jeffreypalermo.com, 2008.
- livro Agile Software Development: Principles, Patterns, and Practices — Robert C. Martin. Prentice Hall, 2002.
- livro Implementing Domain-Driven Design — Vaughn Vernon. Addison-Wesley, 2013.
- artigo Clean Architecture series — Herberto Graça. herbertograca.com, 2017-2018.
- artigo Functional architecture: ports and adapters — Mark Seemann. blog.ploeh.dk, 2016.
- livro Domain-Driven Design — Eric Evans. Addison-Wesley, 2003.
- artigo Presentation Model — Martin Fowler. martinfowler.com, 2004.
- livro Domain Modeling Made Functional — Scott Wlaschin. Pragmatic Bookshelf, 2018.
- livro Code That Fits in Your Head — Mark Seemann. Addison-Wesley, 2021.