O cartão de crédito
O cartão tem a mesma 'interface' do dinheiro: você paga com ele em qualquer lugar que aceite dinheiro. Mas ele não é o dinheiro — é um substituto que controla o acesso à sua conta. Ele verifica limite antes de deixar passar, registra cada compra num extrato e evita que você carregue a quantia toda no bolso. O lojista não muda nada no que faz; para ele, pagamento é pagamento.
O problema
Alguns objetos custam caro para existir: uma imagem em alta resolução, uma conexão de banco, um relatório de milhões de linhas. Criar todos antecipadamente desperdiça memória e tempo com o que talvez nem seja usado.
Outros exigem cerimônia no acesso: checar permissão, registrar auditoria, respeitar rate limit. Espalhar essas checagens por todo chamador é repetição garantida — e uma delas vai ser esquecida.
A tentação é resolver dentro do próprio objeto real: um if (!carregado) carregar() no começo de cada método. Isso mistura a responsabilidade do objeto (exibir a imagem) com a de infraestrutura (gerenciar o carregamento), e o objeto deixa de ser testável sem o disco.
Proxy, Decorator e Adapter são estruturalmente parecidos — todos embrulham um objeto — e a diferença está na intenção, não no desenho. Adapter muda a interface (contratos incompatíveis). Decorator mantém a interface e ADICIONA comportamento, normalmente empilhável. Proxy mantém a interface e CONTROLA o acesso, normalmente sozinho e com a responsabilidade de decidir se a chamada chega.
Um sinal prático: no Decorator, quem monta a pilha é o cliente, e cada camada agrega valor visível ao resultado. No Proxy, o cliente costuma nem saber que existe, e o valor é invisível — mesma resposta, obtida de forma mais barata ou mais segura.
Os quatro sabores
Proxy é um padrão com quatro usos clássicos, todos com a mesma estrutura: virtual (adia a criação), de proteção (checa permissão), remoto (esconde a rede) e cache/smart (memoriza e conta referências).
Virtual é o mais comum: o objeto caro só nasce no primeiro uso. É o que ORMs fazem com lazy loading — pedido.cliente devolve um proxy, e só quando você lê um campo dele é que sai o SELECT.
De proteção filtra por permissão antes de delegar. É a base de muito middleware de autorização: a interface é a mesma, mas a chamada só passa se o chamador puder.
Remoto faz uma chamada de rede parecer local (o 'stub' de RPC/gRPC). Aqui mora a maior armadilha do padrão: a falácia de que a rede é confiável — mais sobre isso nas armadilhas.
Cache/smart guarda o resultado, conta referências ou libera recursos. Um memoizador é, no fundo, um proxy de cache sobre uma função.
Clientechama exibir() — vê só a interface Imagem
ImagemProxymesma interface; decide se a chamada passa
ImagemRealsó nasce no primeiro exibir()
Com Proxy: paga quem usa
- a galeria monta 100 proxies baratos, sem tocar no disco
- o carregamento acontece no primeiro exibir()
- memória proporcional ao que foi realmente aberto
- nos testes, um proxy falso substitui o real
O custo vira uma indireção a mais e uma latência que aparece no meio da interação, não no início — o usuário vê a lista rápido, mas espera ao abrir.
Navegue pelas camadas
Camada 1/3Cliente
use ← →Camada 1
Cliente
programa contra a interface
Depende só do contrato (`Imagem`), nunca da classe concreta. É essa disciplina que permite trocar o real pelo proxy sem tocar em uma linha do chamador.
No código
const img: Imagem = fabrica.criar('a.png');
img.exibir();Se violar: se o cliente declara `ImagemReal` em vez de `Imagem`, o proxy não encaixa e o padrão morre no tipo.
Estrutura
Código
interface Imagem {
exibir(): void;
}
// Objeto real: caro de criar (le o arquivo do disco no construtor)
class ImagemReal implements Imagem {
constructor(private arquivo: string) {
console.log("carregando do disco:", arquivo); // custo alto
}
exibir(): void {
console.log("exibindo", this.arquivo);
}
}
// Proxy virtual: mesma interface, adia a criacao ate o primeiro uso
class ImagemProxy implements Imagem {
private real?: ImagemReal;
constructor(private arquivo: string) {}
exibir(): void {
// so agora o objeto caro nasce — e uma vez so
this.real ??= new ImagemReal(this.arquivo);
this.real.exibir();
}
}
// A galeria monta 100 itens sem tocar no disco
const galeria: Imagem[] = ["a.png", "b.png"].map((f) => new ImagemProxy(f));
galeria[0].exibir(); // carrega so a que o usuario abriufrom abc import ABC, abstractmethod
class Imagem(ABC):
@abstractmethod
def exibir(self): ...
# Objeto real: caro de criar
class ImagemReal(Imagem):
def __init__(self, arquivo):
self._arquivo = arquivo
print("carregando do disco:", arquivo) # custo alto
def exibir(self):
print("exibindo", self._arquivo)
# Proxy virtual: mesma interface, adia a criacao
class ImagemProxy(Imagem):
def __init__(self, arquivo):
self._arquivo = arquivo
self._real = None
def exibir(self):
if self._real is None: # so agora o objeto caro nasce
self._real = ImagemReal(self._arquivo)
self._real.exibir()
galeria = [ImagemProxy(f) for f in ("a.png", "b.png")]
galeria[0].exibir() # carrega so a que o usuario abriuCasos de uso
Lazy loading em ORM
Cenário
Ao carregar um pedido, o ORM poderia trazer cliente, itens, pagamentos e histórico — dezenas de joins que quase nenhuma tela usa por completo.
Como o padrão entra
O ORM devolve proxies nas associações: pedido.cliente é um objeto vazio com a interface de Cliente. O SELECT só dispara quando algum campo é lido de fato. Hibernate, Doctrine e o Prisma fazem variações disso.
Trade-off: É a origem clássica do problema N+1: um laço sobre 200 pedidos lendo p.cliente.nome dispara 200 queries. E acessar um proxy fora da sessão/transação estoura erro de lazy initialization — o custo escondido reaparece no pior momento.
Autorização em API interna
Cenário
Um serviço de RH expõe operações sobre folha de pagamento; cada uma exige um papel diferente, e a checagem precisa acontecer sempre, sem exceção.
Como o padrão entra
Um FolhaProxy implementa a mesma interface do serviço e, antes de delegar, valida o papel do chamador e registra a tentativa na auditoria. A regra de acesso fica num lugar só, e o serviço real continua sem saber o que é um usuário.
Trade-off: Se a decisão de permissão depender de dados do próprio resultado ('só pode ver salários da sua equipe'), o proxy precisa executar a chamada para então filtrar — e a economia de não executar desaparece.
Stub de gRPC
Cenário
Um serviço de catálogo precisa consultar preços em outro serviço, que roda em outra máquina, com serialização e rede no meio.
Como o padrão entra
O stub gerado pelo gRPC é um proxy remoto: expõe precos.consultar(sku) como se fosse um objeto local e esconde serialização, conexão e desserialização.
Trade-off: A conveniência mente sobre a física: a chamada parece local mas pode levar 200ms, falhar por timeout ou por partição de rede. Código escrito como se fosse local (num laço, sem timeout, sem retry) vira incidente em produção.
- 1
Compartilhar a interface
Proxy e objeto real implementam o mesmo contrato. Sem isso, o cliente distingue os dois e o padrão morre no tipo.
- 2
Guardar a política no proxy
Lazy, cache, permissão ou rede moram aqui — finos, sem regra de negócio. O real continua fazendo só o trabalho dele.
- 3
Adiar o custo real
No virtual: o caro só nasce no primeiro uso. Criar o real no construtor do proxy anula o ganho inteiro.
- 4
Entregar o proxy ao cliente
Quem consome programa contra a interface e não precisa saber que existe intermediário.
O Proxy que carrega no construtor
Compila, roda, passa no code review. E quebra depois.
A classe se chama Proxy, implementa a interface certa e até aparece no diagrama — mas instancia o objeto caro no construtor. É um repasse com nome sofisticado; o lazy loading nunca aconteceu.
interface Imagem { exibir(): void; }
class ImagemReal implements Imagem {
constructor(private arquivo: string) {
console.log("carregando do disco:", arquivo);
}
exibir() { console.log("exibindo", this.arquivo); }
}
// "Proxy virtual"... que carrega no construtor.
class ImagemProxy implements Imagem {
private real: ImagemReal;
constructor(arquivo: string) {
this.real = new ImagemReal(arquivo); // <- paga o custo sempre
}
exibir() { this.real.exibir(); } // so repassa
}
// A galeria de 100 itens volta a ler 100 arquivos no boot.
// Mesma interface, zero controle de acesso — casca vazia.O que quebra
- No boot da galeriaCem arquivos são lidos antes de qualquer clique — exatamente o custo que o proxy prometia adiar.
- Na memóriaO consumo fica proporcional ao catálogo inteiro, não ao que o usuário abriu.
- No testeMontar a lista já exige disco/rede; o falso barato que o proxy deveria permitir não existe.
A saída: Guarde só o caminho (ou a factory) no proxy. O ImagemReal nasce no primeiro exibir() — this.real ??= new ImagemReal(...) — e uma vez só.
Armadilhas
Fingir que a rede não existe
O proxy remoto é o padrão mais perigoso da lista justamente porque funciona bem demais: a chamada parece local. Mas latência, falha parcial e timeout continuam lá. Um proxy remoto sem timeout explícito, retry com backoff e circuit breaker transforma a lentidão do vizinho na sua indisponibilidade.
Confundir com Decorator
Os dois embrulham mantendo a interface, então a escolha é de intenção. Se você está adicionando comportamento que o cliente pediu e quer empilhar (log + retry + cache), é Decorator. Se está controlando SE e QUANDO a chamada chega ao objeto, invisivelmente, é Proxy. Chamar tudo de proxy faz o nome perder o poder de comunicar.
Cache sem invalidação
Um proxy de cache é trivial de escrever e traiçoeiro de manter: sem política de expiração ou invalidação, ele serve dado velho para sempre — e o bug aparece longe, como 'o sistema mostra o preço antigo'. Quem coloca cache assume a dívida de decidir quando ele mente.
Quando usar × quando evitar
Use quando
3- O objeto real é caro de criar e nem sempre será usado.
- O acesso precisa de política transversal: permissão, auditoria, rate limit, cache.
- O objeto vive em outro processo e você quer isolar a rede num ponto só.
Evite quando
3- O objeto é barato — sobra indireção e falta ganho.
- Você quer mudar a interface (é Adapter) ou somar comportamento empilhável (é Decorator).
- A política depende do resultado da chamada, o que anula o ganho de interceptá-la.
Testar o que ficou
3 armadilhas de Proxy viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.
Onde isto aparece de verdade
Você já usa isto, talvez sem saber o nome.
O objeto Proxy do JSO padrão virou palavra-chave: você intercepta leitura, escrita e chamada sem tocar no alvo.Reatividade do Vue 3O objeto reativo é um Proxy que avisa o framework a cada acesso e a cada escrita.Lazy loading de ORMpedido.clienteparece um objeto e só vai ao banco quando alguém realmente lê um campo.
O padrão em uma linha
Sem nome de domínio, sem cerimônia. Só a ideia.
// Controla o acesso ao real (lazy, cache, auth).
const imagem = new ProxyImagem("foto.png");
imagem.desenhar(); // carrega sob demandaO que ele cobra
1 salto a mais entre a chamada e o efeito.
- Uma indireção a mais entre o cliente e o objeto real, que o leitor precisa atravessar
- Comportamento escondido no proxy (cache, lazy, log) surpreende quem espera a chamada direta
Não vale se: não há nada a interceptar — sem controle de acesso, cache ou carga tardia, o proxy é só uma casca vazia.
Costuma ser confundido com
Onde isto aparece
Continue por aqui
- DecoratorAdiciona comportamento a um objeto em tempo de execução envolvendo-o em camadas empilháveis que compartilham a mesma interface — composição no lugar de herança.
- AdapterConverte a interface de uma classe em outra que o cliente espera, permitindo que classes incompatíveis trabalhem juntas.
- FacadeOferece uma interface simples e de alto nível para um subsistema complexo, escondendo a coreografia entre as peças atrás de um único ponto de entrada.