MÓDULO 15 · CONCEITO 02 DE 12

Layered Architecture — por que funciona e por que falha

A anatomia da arquitetura em camadas, a regra de dependência, as violações mais comuns e o caminho para o Big Ball of Mud

Tempo de leitura ~20 min Pré-requisito 01 · Estilos Arquiteturais Próximo 03 · Hexagonal / Ports & Adapters

Layered Architecture é provavelmente o estilo arquitetural mais amplamente adotado na história do desenvolvimento de software — e também o mais frequentemente mal-implementado. É o estilo que a maioria dos frameworks web ensina implicitamente, o que a maioria dos tutoriais assume, e o que a maioria dos times adota por padrão sem nunca tomar essa decisão conscientemente. Isso torna entendê-lo profundamente mais importante do que entender estilos mais sofisticados: você vai se deparar com arquitetura em camadas em praticamente todo sistema que vai manter.

A estrutura fundamental

A ideia central é organizar o sistema em camadas horizontais, onde cada camada tem uma responsabilidade bem definida e depende apenas das camadas abaixo. A estrutura canônica de quatro camadas, popularizada por Eric Evans em Domain-Driven Design (2003) e anterior a ele em Fowler (2002), é:

Presentation Layer (ou Interface Layer): responsável por receber entrada do usuário ou de sistemas externos e apresentar saída. Nessa camada vivem controllers HTTP, handlers de fila, CLIs, e tudo que traduz o mundo externo para o vocabulário interno do sistema. A Presentation não deve conter lógica de negócio — apenas tradução e orquestração.

Application Layer (ou Service Layer): responsável por coordenar o que o sistema faz — os casos de uso. Aqui vivem Application Services, Use Cases, Command Handlers. A Application Layer orquestra: ela chama objetos de domínio, chama repositórios, dispara eventos. Ela não decide regras de negócio; ela coordena quem decide.

Domain Layer: o coração do sistema. Aqui vivem as Entities, Value Objects, Aggregates, Domain Services, Domain Events — tudo que representa o problema que o sistema resolve. É a camada mais estável: muda quando o negócio muda, não quando a tecnologia muda. Em uma arquitetura em camadas saudável, o Domain não conhece nada das camadas acima nem da Infrastructure abaixo.

Infrastructure Layer: o maquinário técnico que suporta tudo acima. Aqui vivem implementações de repositórios (PostgreSQL, MongoDB), clientes HTTP para serviços externos, envio de email, publicação em filas, acesso a filesystem. A Infrastructure depende de Domain (implementa interfaces definidas lá), mas Domain nunca depende de Infrastructure.

A regra de dependência

O que separa Layered Architecture de código organizado em pastas é a regra de dependência: dependências só apontam para baixo. Presentation pode depender de Application; Application pode depender de Domain; Infrastructure pode depender de Domain (para implementar suas interfaces). Nenhuma camada pode depender de uma camada acima.

Essa regra tem uma implicação importante: Domain não pode depender de Infrastructure. Isso significa que Domain não pode chamar repositórios diretamente, não pode chamar clientes HTTP, não pode usar bibliotecas de ORM. Para que Domain possa pedir a persistência de algo, ele define uma interface (um contrato) — e Infrastructure a implementa. A inversão de dependência (o "D" do SOLID) é o mecanismo que permite que a regra seja mantida.

a regra em uma frase

Domain define as interfaces; Infrastructure as implementa; Application orquestra; Presentation traduz. Ninguém chama quem está acima.

Por que funciona

Quando a regra de dependência é mantida, as vantagens são concretas. Mudanças de tecnologia ficam confinadas à Infrastructure: trocar PostgreSQL por MySQL, mudar de REST para gRPC, migrar de SendGrid para SES — tudo sem tocar em Domain ou Application. Testes de domínio ficam mais simples: se Domain não conhece Infrastructure, você pode testar regras de negócio sem instanciar banco ou serviços externos.

O onboarding é mais rápido porque a estrutura é previsível. Um engenheiro novo em um sistema layered sabe que vai encontrar controllers em Presentation, use cases em Application, e entidades em Domain. O modelo mental é intuitivo mesmo para quem nunca viu o sistema antes.

Por que falha

O problema mais comum com Layered Architecture não é teórico — é prático e gradual. Começa com uma decisão aparentemente inofensiva: "vou chamar o repositório direto do Domain por enquanto para simplificar". Depois de seis meses, Domain chama repositórios, clientes HTTP, e até envia email. A regra de dependência está morta.

Esse padrão tem nome: Lasagna Code — camadas que existem no papel mas são opacas na prática. Ou, no extremo mais grave, Big Ball of Mud (Foote e Yoder, 1997): um sistema onde qualquer coisa pode chamar qualquer coisa, onde não há regra de dependência observável, e onde cada mudança tem efeitos colaterais imprevisíveis.

Outras formas de falha documentadas:

Anemic Domain Model (Fowler, 2003): Domain tem objetos que são apenas estruturas de dados — sem métodos, sem comportamento, sem invariantes. A lógica de negócio vaza para Application Layer, que cresce até se tornar um conjunto de procedimentos que manipulam objetos passivos. O resultado é código difícil de entender e de testar.

Fat Controllers: a Presentation Layer absorve lógica de negócio porque é mais fácil colocar o código no controller do que criar um Application Service adequado. Testar a lógica requer simular HTTP; o controller vira monolito.

Tight Layer Coupling: Application chama Infrastructure diretamente (sem passar por interfaces definidas em Domain), criando dependência que deveria ser invertida. Testar Application requer banco de dados real.

Implementação em três linguagens

C# — Domain, Application e Infrastructure em .NET
// Domain Layer — sem dependência de Infrastructure
namespace Orders.Domain
{
    public class Order // Entity
    {
        public OrderId Id { get; private set; }
        public CustomerId CustomerId { get; private set; }
        private readonly List<OrderLine> _lines = new();
        public OrderStatus Status { get; private set; }

        public void AddLine(ProductId productId, int qty, Money price)
        {
            if (Status != OrderStatus.Draft)
                throw new InvalidOperationException("Pedido já confirmado");
            _lines.Add(new OrderLine(productId, qty, price));
        }

        public void Confirm()
        {
            if (!_lines.Any())
                throw new InvalidOperationException("Pedido vazio");
            Status = OrderStatus.Confirmed;
        }
    }

    // Domain define a interface; Infrastructure a implementa
    public interface IOrderRepository
    {
        Task<Order?> FindByIdAsync(OrderId id);
        Task SaveAsync(Order order);
    }
}

// Application Layer — orquestra, não decide regras
namespace Orders.Application
{
    public class ConfirmOrderCommand { public OrderId OrderId { get; init; } }

    public class ConfirmOrderHandler
    {
        private readonly IOrderRepository _orders; // interface do Domain

        public ConfirmOrderHandler(IOrderRepository orders) => _orders = orders;

        public async Task HandleAsync(ConfirmOrderCommand cmd)
        {
            var order = await _orders.FindByIdAsync(cmd.OrderId)
                        ?? throw new NotFoundException(cmd.OrderId);
            order.Confirm(); // regra de negócio no Domain
            await _orders.SaveAsync(order);
        }
    }
}

// Infrastructure Layer — implementa o contrato do Domain
namespace Orders.Infrastructure
{
    public class PostgresOrderRepository : IOrderRepository
    {
        private readonly AppDbContext _db;
        public PostgresOrderRepository(AppDbContext db) => _db = db;

        public async Task<Order?> FindByIdAsync(OrderId id)
            => await _db.Orders.FindAsync(id.Value);

        public async Task SaveAsync(Order order)
        {
            _db.Orders.Update(order);
            await _db.SaveChangesAsync();
        }
    }
}

Domain define a interface; Infrastructure implementa. Application orquestra — a regra de dependência é mantida via inversão de dependência.

Python — ABCs como contratos de camada, asyncpg em Infrastructure
# Domain Layer — sem import de infra
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
from abc import ABC, abstractmethod

class OrderStatus(Enum):
    DRAFT = "draft"
    CONFIRMED = "confirmed"

@dataclass
class Order:
    id: str
    customer_id: str
    lines: list = field(default_factory=list)
    status: OrderStatus = OrderStatus.DRAFT

    def add_line(self, product_id: str, qty: int, price: float) -> None:
        if self.status != OrderStatus.DRAFT:
            raise ValueError("Pedido já confirmado")
        self.lines.append({"product_id": product_id, "qty": qty, "price": price})

    def confirm(self) -> None:
        if not self.lines:
            raise ValueError("Pedido vazio")
        self.status = OrderStatus.CONFIRMED

class OrderRepository(ABC):  # contrato do Domain
    @abstractmethod
    async def find_by_id(self, order_id: str) -> Order | None: ...
    @abstractmethod
    async def save(self, order: Order) -> None: ...

# Application Layer — orquestra
class ConfirmOrderUseCase:
    def __init__(self, orders: OrderRepository):
        self._orders = orders

    async def execute(self, order_id: str) -> None:
        order = await self._orders.find_by_id(order_id)
        if order is None:
            raise ValueError(f"Order {order_id} not found")
        order.confirm()
        await self._orders.save(order)

# Infrastructure Layer — implementa contrato
import asyncpg

class PostgresOrderRepository(OrderRepository):
    def __init__(self, pool: asyncpg.Pool):
        self._pool = pool

    async def find_by_id(self, order_id: str) -> Order | None:
        async with self._pool.acquire() as conn:
            row = await conn.fetchrow(
                "SELECT * FROM orders WHERE id = $1", order_id
            )
            return _to_order(row) if row else None

    async def save(self, order: Order) -> None:
        async with self._pool.acquire() as conn:
            await conn.execute(
                "UPDATE orders SET status=$1 WHERE id=$2",
                order.status.value, order.id
            )

ABCs definem contratos de camada. Testes substituem PostgresOrderRepository por implementação em memória — sem banco, sem rede.

Go — packages como fronteiras entre camadas
// domain/order.go — sem import de infra
package domain

import "errors"

type OrderStatus string

const (
    Draft     OrderStatus = "draft"
    Confirmed OrderStatus = "confirmed"
)

type Order struct {
    ID         string
    CustomerID string
    Lines      []OrderLine
    Status     OrderStatus
}

func (o *Order) AddLine(productID string, qty int, price float64) error {
    if o.Status != Draft {
        return errors.New("pedido já confirmado")
    }
    o.Lines = append(o.Lines, OrderLine{productID, qty, price})
    return nil
}

func (o *Order) Confirm() error {
    if len(o.Lines) == 0 {
        return errors.New("pedido vazio")
    }
    o.Status = Confirmed
    return nil
}

// Contrato definido em Domain — Infrastructure implementa
type OrderRepository interface {
    FindByID(ctx context.Context, id string) (*Order, error)
    Save(ctx context.Context, order *Order) error
}

// app/confirm_order.go — Application Layer
package app

type ConfirmOrderCommand struct{ OrderID string }

type ConfirmOrderHandler struct {
    orders domain.OrderRepository
}

func (h *ConfirmOrderHandler) Handle(ctx context.Context, cmd ConfirmOrderCommand) error {
    order, err := h.orders.FindByID(ctx, cmd.OrderID)
    if err != nil { return err }
    if err := order.Confirm(); err != nil { return err }
    return h.orders.Save(ctx, order)
}

// infra/postgres_order_repo.go — Infrastructure Layer
package infra

type PostgresOrderRepository struct{ db *pgxpool.Pool }

func (r *PostgresOrderRepository) FindByID(ctx context.Context, id string) (*domain.Order, error) {
    var o domain.Order
    err := r.db.QueryRow(ctx, "SELECT id,customer_id,status FROM orders WHERE id=$1", id).
        Scan(&o.ID, &o.CustomerID, &o.Status)
    if errors.Is(err, pgx.ErrNoRows) { return nil, nil }
    return &o, err
}

func (r *PostgresOrderRepository) Save(ctx context.Context, order *domain.Order) error {
    _, err := r.db.Exec(ctx, "UPDATE orders SET status=$1 WHERE id=$2",
        order.Status, order.ID)
    return err
}

Packages em Go funcionam como fronteiras de camada. domain.OrderRepository é a interface; infra.PostgresOrderRepository implementa. Testes injetam fake.

Como manter a disciplina ao longo do tempo

Não existe ferramenta mágica. O que funciona é uma combinação de revisão de código com sensibilidade para violações, testes de domínio que detectam quando o domínio passou a depender de infraestrutura, e convenção de equipe sobre o que vai em qual camada.

Algumas ferramentas ajudam: em C# e Java, bibliotecas como ArchUnit e NetArchTest permitem escrever testes que verificam que Domain não importa tipos de Infrastructure. Em Python e Go, onde o compilador não impede importações, a disciplina é mais manual — mas os testes de domínio puro (sem banco) funcionam como verificação indireta: se o teste de domínio precisa de conexão com banco, é sinal de violação da regra de dependência.

A última linha de defesa é a revisão de código. Um PR que adiciona um import de Infrastructure dentro de Domain deve ser bloqueado — não por purismo, mas porque esse import é a semente do Big Ball of Mud.

Referências para aprofundar

  1. livro Domain-Driven Design — Eric Evans. Addison-Wesley, 2003. Cap 4: a definição canônica das quatro camadas e a regra de dependência entre elas.
  2. livro Patterns of Enterprise Application Architecture — Martin Fowler. Addison-Wesley, 2002. Service Layer e Table Module — o contexto original de onde emergiu a arquitetura em camadas.
  3. artigo AnemicDomainModel — Martin Fowler. martinfowler.com, 2003. O anti-padrão mais comum: Domain sem comportamento, lógica de negócio vazando para Application.
  4. paper Big Ball of Mud — Foote & Yoder. PLoP '97, 1997. A descrição clínica de como Layered Architecture degenera sem disciplina de dependências.
  5. livro Clean Architecture — Robert C. Martin. Prentice Hall, 2017. Caps 17-22: a Dependency Rule e por que frameworks e bancos são detalhes periféricos.
  6. artigo The Dependency Inversion Principle — Robert C. Martin. C++ Report, 1996. O mecanismo que permite Domain não depender de Infrastructure via inversão de dependência.
  7. livro Implementing Domain-Driven Design — Vaughn Vernon. Addison-Wesley, 2013. Cap 4: Application Layer e Domain Layer em detalhe com exemplos práticos de código.
  8. artigo The Onion Architecture — Jeffrey Palermo. jeffreypalermo.com, 2008. Variante de Layered com dependências apontando para o núcleo — sem tocar no Domain de fora.
  9. livro Patterns, Principles, and Practices of Domain-Driven Design — Millett & Tune. Wrox, 2015. Cap 11: estruturação de camadas em sistemas DDD na prática com exemplos reais.
  10. livro Dependency Injection in .NET — Mark Seemann. Manning, 2011. IoC como mecanismo que suporta inversão de dependência entre Domain e Infrastructure.
  11. livro Growing Object-Oriented Software, Guided by Tests — Freeman & Pryce. Addison-Wesley, 2009. Como testes revelam violações arquiteturais e guiam as fronteiras corretas de camada.
  12. livro Design Patterns — Gamma et al. Addison-Wesley, 1994. Repository como padrão que separa lógica de domínio de mecanismos de persistência.