MÓDULO 15 · CONCEITO 04 DE 12

Clean Architecture — Uncle Bob 2012

Os quatro anéis concêntricos, a Dependency Rule como lei, e a relação com Hexagonal — mais próximos do que parecem

Tempo de leitura ~19 min Pré-requisito 03 · Hexagonal / Ports & Adapters Próximo 05 · Microservices vs Modular Monolith

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.

a regra em uma frase

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

C# — quatro projetos: Domain, Application, Adapters, Infrastructure
// 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.

Python — packages mapeiam para os anéis concêntricos
# 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.

Go — internal/domain, app, adapters/http, adapters/postgres
// 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

  1. artigo The Clean Architecture — Robert C. Martin. blog.cleancoder.com, 2012. O artigo original com o diagrama dos quatro anéis e a Dependency Rule formulada.
  2. livro Clean Architecture — Robert C. Martin. Prentice Hall, 2017. O livro completo — todos os princípios SOLID, a Dependency Rule e como aplicar os anéis em sistemas reais.
  3. artigo Hexagonal Architecture — Alistair Cockburn. alistair.cockburn.us, 2005. A origem de muitas das ideias que Clean Architecture formalizou — ports, adapters e isolamento do domínio.
  4. artigo The Onion Architecture — Jeffrey Palermo. jeffreypalermo.com, 2008. Variante com nomenclatura diferente — camadas internas e externas com o mesmo princípio de dependências.
  5. livro Agile Software Development: Principles, Patterns, and Practices — Robert C. Martin. Prentice Hall, 2002. SOLID na origem — os princípios que fundamentam por que a Dependency Rule funciona.
  6. livro Implementing Domain-Driven Design — Vaughn Vernon. Addison-Wesley, 2013. Clean Architecture aplicada em DDD — como Entities e Use Cases se encaixam nos bounded contexts.
  7. artigo Clean Architecture series — Herberto Graça. herbertograca.com, 2017-2018. Série detalhada comparando Clean Architecture, Hexagonal, Onion e outros estilos relacionados.
  8. artigo Functional architecture: ports and adapters — Mark Seemann. blog.ploeh.dk, 2016. Clean Architecture em paradigma funcional — como a Dependency Rule se traduz para funções puras.
  9. livro Domain-Driven Design — Eric Evans. Addison-Wesley, 2003. A fonte dos conceitos de Entity e domain rules que habitam os anéis internos de Clean Architecture.
  10. artigo Presentation Model — Martin Fowler. martinfowler.com, 2004. O Presenter como conceito — base do output port que Clean Architecture formaliza.
  11. livro Domain Modeling Made Functional — Scott Wlaschin. Pragmatic Bookshelf, 2018. Clean Architecture em F# — como os anéis se expressam em linguagem funcional sem herança.
  12. livro Code That Fits in Your Head — Mark Seemann. Addison-Wesley, 2021. Como aplicar Clean Architecture sem over-engineering — o ponto de equilíbrio entre pureza e pragmatismo.