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.
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
// 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.
# 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.
// 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
- livro Domain-Driven Design — Eric Evans. Addison-Wesley, 2003.
- livro Patterns of Enterprise Application Architecture — Martin Fowler. Addison-Wesley, 2002.
- artigo AnemicDomainModel — Martin Fowler. martinfowler.com, 2003.
- paper Big Ball of Mud — Foote & Yoder. PLoP '97, 1997.
- livro Clean Architecture — Robert C. Martin. Prentice Hall, 2017.
- artigo The Dependency Inversion Principle — Robert C. Martin. C++ Report, 1996.
- livro Implementing Domain-Driven Design — Vaughn Vernon. Addison-Wesley, 2013.
- artigo The Onion Architecture — Jeffrey Palermo. jeffreypalermo.com, 2008.
- livro Patterns, Principles, and Practices of Domain-Driven Design — Millett & Tune. Wrox, 2015.
- livro Dependency Injection in .NET — Mark Seemann. Manning, 2011.
- livro Growing Object-Oriented Software, Guided by Tests — Freeman & Pryce. Addison-Wesley, 2009.
- livro Design Patterns — Gamma et al. Addison-Wesley, 1994.