Contratos, status e erros de terceiros se espalham pelo código aos poucos. Veja como uma Anti-Corruption Layer isola esse acoplamento e quando ela compensa.


Sabe aquela integração que começa com uma chamada HTTP, um DTO de resposta e uma verificação de status?
A aplicação precisa cobrar uma assinatura. O gateway retorna SETTLED, o código ativa o plano e tudo parece resolvido.
Só que a integração cresce. Entram Pix, boleto, cobrança recorrente, cancelamento, estorno e webhooks. Aos poucos, respostas do fornecedor começam a aparecer nos casos de uso, exceções específicas são tratadas em vários pontos e status como AWAITING_PAYMENT, OVERDUE e REFUNDED, que nem fazem parte da linguagem da aplicação, passam a orientar decisões importantes do domínio.
O problema não é depender de um sistema externo. É natural, e muitas vezes necessário, usar APIs de terceiros.
O problema começa quando a aplicação deixa de apenas conversar com esse sistema e passa a pensar como ele.
Uma mudança no contrato externo agora exige alterações em diferentes partes do código. Trocar de fornecedor deixa de ser apenas substituir uma integração. É preciso encontrar todos os pontos que passaram a conhecer seus nomes, formatos, códigos e regras.
A Anti-Corruption Layer cria uma fronteira entre esses modelos. Em vez de permitir que contratos externos atravessem a aplicação, ela traduz status, dados, erros e operações para conceitos que façam sentido internamente e estejam alinhados à linguagem que a aplicação já usa.
O padrão foi descrito originalmente no contexto do Domain-Driven Design, mas o problema que ele resolve aparece em qualquer sistema que precise se integrar com gateways, APIs de terceiros, sistemas legados, SDKs ou serviços de outras equipes.
Neste artigo, vamos entender como essa fronteira funciona, por que ela é mais do que um simples mapper e quando seu custo realmente compensa.
TL;DR
Uma Anti-Corruption Layer pertence ao contexto consumidor que deseja preservar seu próprio modelo.
Ela traduz contratos, status, erros e operações externas para conceitos internos, evitando que detalhes de fornecedores, sistemas legados ou outros serviços se espalhem pela aplicação.
Use uma ACL quando existir uma diferença semântica relevante. Quando os dois lados já falam praticamente a mesma linguagem, um client ou adapter simples pode ser suficiente.
A ACL concentra o acoplamento, mas não o elimina. Em troca dessa proteção, exige traduções, testes e manutenção da fronteira.
Uma Anti-Corruption Layer, ou ACL, é uma fronteira de tradução entre a aplicação e um modelo externo.
Ela permite que dois sistemas se comuniquem sem obrigar um deles a adotar os contratos, nomes e regras do outro.
ACL também pode significar Access Control List, a lista de permissões usada em segurança e redes. Aqui, porém, significa sempre Anti-Corruption Layer.
Um gateway pode retornar SETTLED, enquanto a aplicação trabalha com PAID. A ACL conhece os dois significados e faz a tradução entre eles.
O modelo externo não precisa estar errado ou ser mal projetado. Ele pode ser perfeitamente adequado ao contexto em que foi criado e, ainda assim, não representar o problema da forma que faz sentido dentro da nossa aplicação.
A ACL não tenta criar um modelo universal. Ela permite que cada lado continue falando sua própria linguagem, enquanto a fronteira traduz o necessário para que ambos consigam se comunicar.
Nesse contexto, corrupção não tem relação com segurança, fraude ou dados danificados.
Ela acontece quando um conceito que faz sentido do outro lado passa a determinar como o nosso modelo deve ser estruturado.
Na prática, isso começa pequeno:
Repare no detalhe: SETTLED é um status do fornecedor, não um conceito da nossa aplicação. Ele pode fazer sentido para o gateway, mas não necessariamente para o domínio de assinaturas.
if ("SETTLED".equals(payment.status())) {
subscription.activate();
}Agora a regra de ativação da assinatura depende diretamente do vocabulário do fornecedor.
Com a tradução:
if (payment.isPaid()) {
subscription.activate();
}Nesse ponto é justo desconfiar: isPaid() não apareceu do nada, e isso não é apenas esconder a mesma comparação dentro de outro método.
A diferença está em onde permanece o conhecimento de que SETTLED significa um pagamento concluído para este contexto.
O vocabulário externo fica na tradução feita pela ACL:
private PaymentStatus translateStatus(String externalStatus) {
return switch (externalStatus) {
case "SETTLED", "RECEIVED" -> PaymentStatus.PAID;
case "AWAITING_PAYMENT" -> PaymentStatus.PENDING;
case "OVERDUE" -> PaymentStatus.PAST_DUE;
case "REFUNDED" -> PaymentStatus.REFUNDED;
default -> throw new UnsupportedProviderStatusException(
externalStatus
);
};
}Esse switch pertence ao translator da ACL. É nele que o vocabulário do fornecedor é convertido para o modelo interno; o caso de uso não precisa conhecer esses status externos.
O comportamento isPaid() pertence ao modelo interno PaymentResult. Ele consulta o PaymentStatus que já foi convertido pela ACL:
public boolean isPaid() {
return status == PaymentStatus.PAID;
}Repare que isPaid() não esconde uma comparação com SETTLED: ele nem sabe que essa string existe.
A ACL interpreta o vocabulário externo. O modelo interno decide o comportamento associado a PAID.
Se outro fornecedor chama o mesmo resultado de SUCCEEDED, ele recebe sua própria tradução. O restante da aplicação continua trabalhando com PaymentStatus.PAID.
Dois fornecedores, dois vocabulários e um conceito interno.
A corrupção não está no modelo externo.
Ela acontece quando um conceito criado para outro contexto passa a determinar a linguagem e as decisões da nossa aplicação.
Uma ACL é construída do ponto de vista do contexto que deseja preservar seu modelo.
Em uma integração com um gateway:
O sistema upstream define o contrato oferecido. O downstream consome esse contrato, mas não precisa copiá-lo para dentro de seu próprio modelo.
A ACL pertence ao lado consumidor:
Embora possa traduzir informações nos dois sentidos, ela não é uma fronteira neutra. Sua função é proteger o modelo downstream.
A tradução acontece sempre que a fronteira é atravessada:
| Fluxo | ACL recebe | ACL entrega |
|---|---|---|
| Aplicação → Gateway | PaymentRequest interno | Requisição esperada pelo gateway |
| Gateway → Aplicação | Resposta externa | PaymentResult interno |
| Gateway → Aplicação | Erro do fornecedor | Falha compreensível pela aplicação |
| Gateway → Aplicação | Webhook externo | Evento interno |
Depois de cada travessia, o restante da aplicação continua trabalhando apenas com sua própria linguagem.
Imagine que a aplicação precise apenas do identificador e do estado de uma cobrança:
public record PaymentResult(
PaymentId id,
PaymentStatus status
) {}O gateway, porém, pode retornar um contrato muito maior:
public record ProviderAPaymentResponse(
String id,
String status,
String billingType,
String invoiceUrl,
String receiptUrl,
String estimatedCreditDate,
String providerAccount
) {}A aplicação não precisa reproduzir tudo isso.
A ACL seleciona o que é relevante e impede que o modelo interno se transforme em uma cópia da API externa. Por isso, além de traduzir, ela também funciona como um filtro.
A tradução pode ser técnica ou semântica, e cada uma resolve um problema diferente:
| Tipo | Exemplo | O que muda |
|---|---|---|
| Técnica | JSON → XML | Formato da mensagem. |
| Técnica | REST → SOAP | Protocolo ou estilo de comunicação. |
| Técnica | HTTP → mensageria | Forma como a informação é transportada. |
| Semântica | SETTLED → PAID | Significado usado dentro da aplicação. |
| Semântica | customer → subscriber | Linguagem usada para representar o conceito. |
| Semântica | código B → conta bloqueada | Interpretação de um valor externo. |
Uma ACL pode realizar as duas.
Ela pode receber XML por SOAP, converter o payload para objetos Java e, depois, interpretar o código A como CustomerStatus.ACTIVE.
Mas a tradução técnica, sozinha, não caracteriza necessariamente uma ACL. Um também pode converter protocolos.
O papel central da ACL é proteger o significado usado pelo modelo interno.
Na prática, essa fronteira pode ser formada por:
Esses componentes não são obrigatórios nem precisam existir em classes separadas.
Em uma integração pequena, o adapter pode fazer também a tradução. Em uma integração extensa, separar client, adapter e translator pode melhorar a clareza.
A complexidade da estrutura deve acompanhar a complexidade real do problema.
Port, adapter e ACL não são sinônimos. No exemplo abaixo, o PaymentGateway define o contrato interno; o ProviderAPaymentAdapter o implementa contra o fornecedor; e a ACL é a fronteira arquitetural formada por essas peças e pela tradução semântica.
| Elemento no exemplo | Papel | Relação com a ACL |
|---|---|---|
PaymentGateway | Port | Define o contrato que a aplicação quer consumir. |
ProviderAPaymentAdapter | Adapter | Implementa o port usando o contrato do fornecedor. |
ProviderAPaymentTranslator | Translator semântico | Converte contratos, status e erros externos em conceitos internos. |
| Anti-Corruption Layer | Fronteira arquitetural | Reúne essas peças para isolar o contrato externo. |
Criar uma interface e um adapter não significa automaticamente criar uma ACL.
Um adapter que devolve ProviderAPaymentResponse para o caso de uso continua permitindo que o contrato externo atravesse a aplicação.
A diferença está na intenção arquitetural e na proteção semântica.
Alguns sinais mostram que a fronteira está vazando:
// Tipo do fornecedor vazando na assinatura
public ProviderAPaymentResponse createSubscription(...) {
// ...
}
// Exceção externa tratada dentro do caso de uso
catch (ProviderAException exception) {
// ...
}
// Código externo decidindo uma regra interna
if ("PAYMENT_SETTLED".equals(event.status())) {
// ...
}Tipos, enums, códigos e exceções do fornecedor não deveriam aparecer em controllers, entidades ou casos de uso internos.
Afinal, eles representam regras e detalhes do provedor externo, não conceitos que a nossa aplicação deveria conhecer.
Isso não elimina o acoplamento. A ACL continua conhecendo o contrato externo.
O ganho está em concentrar esse acoplamento em uma fronteira controlada, em vez de deixá-lo se espalhar.
Assim como na Lei de Deméter, buscamos limitar o conhecimento entre as partes. Aqui, porém, a fronteira está entre modelos e sistemas, não entre objetos.
Uma ACL não remove a dependência externa. Ela impede que essa dependência determine como o restante da aplicação deve pensar.
ACL não aparece apenas em sistemas legados ou projetos que adotam DDD formalmente.
Ela pode ser útil sempre que dois sistemas precisam colaborar, mas representam o problema de formas diferentes.
Gateways de pagamento são um cenário comum.
Cada fornecedor pode ter contratos, status, exceções, webhooks e capacidades diferentes. A ACL traduz essas diferenças antes que elas cheguem à aplicação.
Os status abaixo são fictícios e simplificados. Gateways reais possuem ciclos de vida próprios e não devem ser forçados a equivalências que o negócio não reconhece.
| Nosso modelo | Gateway A | Gateway B |
|---|---|---|
PAID | SETTLED, RECEIVED | SUCCEEDED, COMPLETED |
PENDING | AWAITING_PAYMENT | PROCESSING |
PAST_DUE | OVERDUE | PAST_DUE |
REFUNDED | REFUNDED | REVERSED |
Neste contexto de assinaturas, PAID significa que o pagamento atingiu o estado necessário para liberar o acesso.
Um contexto de conciliação financeira poderia preservar diferenças mais detalhadas entre confirmação, captura e liquidação.
Isso mostra um ponto importante: a ACL não traduz o dado de forma universal. Ela traduz de acordo com a pergunta que o contexto consumidor precisa responder.
Um novo fornecedor pode ser adicionado sem alterar o caso de uso enquanto o contrato interno continuar representando corretamente as capacidades exigidas pelo negócio.
A ACL concentra as diferenças, mas não transforma fornecedores semanticamente diferentes em alternativas perfeitamente intercambiáveis.
Se um oferece estorno parcial, autorização e captura separadas ou split de pagamentos, enquanto outro não oferece, talvez o contrato interno precise refletir essas diferenças.
Esse princípio também vale para serviços de e-mail, armazenamento em cloud, transportadoras, provedores de identidade e provedores de IA com seus SDKs.
Numa modernização, a ACL permite que o sistema novo trabalhe com o próprio modelo enquanto ainda consulta dados e executa operações no legado.
O legado pode representar clientes assim:
{
"cod_cli": "1029",
"sit": "A",
"tp_cli": "02",
"fl_pend": "S"
}Um pouco difiícil de lidar com strings e códigos opacos né? Por isso o sistema moderno prefere trabalhar com:
public record Customer(
CustomerId id,
CustomerStatus status,
CustomerType type,
boolean hasPendingIssues
) {}É aqui onde a ACL brilha e interpreta:
| Legado | Modelo interno | O que a tradução resolve |
|---|---|---|
cod_cli: "1029" | CustomerId | Uma string solta ganha tipo e significado. |
sit: "A" | CustomerStatus.ACTIVE | Um código opaco vira um estado nomeado. |
tp_cli: "02" | CustomerType.BUSINESS | Um número mágico vira um conceito do domínio. |
fl_pend: "S" | hasPendingIssues = true | Uma flag textual vira booleano. |
O sistema novo continua usando seu próprio modelo sem copiar nomes, códigos e limitações históricas.
A ACL também permite que a migração aconteça por partes. Algumas operações continuam no legado enquanto outras já foram assumidas pelo sistema moderno.
Em migrações, essa fronteira pode nascer com um critério explícito de remoção. Quando o legado for completamente substituído, a camada deixa de ter função.
Dois serviços internos também podem representar o mesmo processo de formas diferentes.
Imagine um serviço de estoque publicando:
{
"sku": "ABC-123",
"availabilityCode": 2,
"warehouse": "REC-01"
}O contexto de pedidos talvez precise apenas saber se o item pode ser reservado. Na fronteira, o código externo ganha um significado interno:
A ACL traduz a mensagem antes de entregá-la ao modelo de pedidos.
Essa comunicação pode acontecer por REST, gRPC, eventos ou filas. A tradução de protocolo pode fazer parte da fronteira, mas não substitui a tradução de significado.
Outro contexto pode interpretar os mesmos dados de forma diferente:
Infraestrutura técnica pode ser compartilhada: autenticação, client HTTP, rate limiting e telemetria.
A tradução semântica, porém, costuma permanecer próxima do contexto consumidor, porque diferentes contextos podem interpretar o mesmo dado externo de formas diferentes.
Uma ACL central e compartilhada pode existir, mas precisa possuir linguagem e próprios.
Caso contrário, ela corre o risco de se transformar em um modelo corporativo genérico que impõe o mesmo significado a todos os consumidores.
Também é importante não esconder as consequências reais do modelo de comunicação.
Se o trabalho entra em uma fila, continuam existindo , , retentativas e falhas tardias.
A ACL traduz o fluxo. Ela não transforma uma mensagem assíncrona em uma chamada síncrona.
Antes de criar uma ACL entre serviços internos, verifique se existe realmente uma incompatibilidade de modelos.
Às vezes, o problema é apenas um contrato instável, ausência de compatibilidade retroativa ou uma relação mal definida entre as equipes.
Uma ACL deve proteger uma diferença concreta, não hipotética.
Criá-la apenas porque existe uma integração pode produzir mais código sem reduzir a complexidade do sistema.
A pergunta mais importante é:
O que exatamente estamos tentando proteger?
Se o time não consegue responder, talvez a camada esteja sendo criada apenas porque parece “arquiteturalmente elegante”. E é aí que mora o perigo do : adicionar estrutura para resolver um problema que ainda não existe.
Uma ACL tende a fazer sentido quando:
Quanto maior a diferença semântica, maior o valor da fronteira:
A existência de uma API externa, sozinha, não é justificativa suficiente.
Nem sempre precisamos manter um modelo próprio.
Se o sistema oferece um contrato estável, com compatibilidade retroativa, e os dois lados usam praticamente os mesmos conceitos, aceitar esse modelo pode ser uma escolha consciente.
No do DDD, essa relação é conhecida como Conformist.
Considere dois contratos em que id, name, email, active e createdAt possuem exatamente o mesmo significado.
Criar um externo, outro interno, um mapper, uma interface e testes para transformações idênticas pode apenas duplicar estruturas.
Preservar um modelo próprio tem valor quando existe algo relevante para preservar.
Aceitar um contrato externo não significa ausência de arquitetura.
Em alguns cenários, significa reconhecer que o custo da tradução seria maior que o benefício.
Uma ACL oferece proteção, mas essa proteção não é gratuita.
| Aspecto | O que podemos ganhar | O que precisamos pagar |
|---|---|---|
| Modelo interno | Linguagem mais clara e independente. | Novos tipos e traduções. |
| Mudanças externas | Impacto concentrado na fronteira. | Manutenção quando qualquer lado muda. |
| Testes | Casos de uso isolados do fornecedor. | Testes de tradução e contrato. |
| Múltiplos fornecedores | Contratos internos estáveis. | Adapters específicos e limites de capacidade. |
| Migração de legado | Modernização gradual. | Coexistência e sincronização entre sistemas. |
| ACL remota | Operação ou escala independentes. | Rede, deploy e outro ponto de falha. |
Também existe o risco de perder informação durante a tradução.
Se o fornecedor possui dez estados e a aplicação reduz todos para três, essa simplificação pode ser útil. Mas também pode esconder diferenças que serão necessárias depois.
Outro sinal de alerta aparece quando vários serviços internos precisam criar ACLs para se proteger de um mesmo sistema.
Pode existir:
Nesse cenário, adapters e DTOs adicionais aliviam o impacto localmente, mas não resolvem a causa.
A arquitetura pode estar escondendo outro problema
Às vezes, a ACL protege uma incompatibilidade real de modelos.
Em outras, ela apenas compensa contratos instáveis ou uma relação mal definida entre equipes.
Antes de adicionar outra camada, vale perguntar se o contrato upstream deveria ser estabilizado, simplificado ou redesenhado.
A pergunta prática é:
A fronteira reduz mais complexidade do que adiciona?
Quando a resposta é não, um client simples, uma tradução localizada ou uma relação conformista podem ser escolhas melhores.
Uma ACL pode viver dentro da própria aplicação ou como um serviço separado. O ponto de partida mais comum é mantê-la interna: isso reduz latência, infraestrutura e deixa o ownership mais simples.
Separá-la por rede só costuma fazer sentido quando vários consumidores compartilham a mesma tradução, existe um time responsável por ela ou há uma necessidade operacional concreta de isolamento e escala independente.
Mesmo assim, compartilhar a fronteira exige compartilhar significado. Se assinaturas, cobrança e conciliação interpretam o fornecedor de formas diferentes, uma ACL central pode impor mais um modelo externo em vez de proteger cada contexto.
Também vale decidir por quanto tempo essa camada deve existir. Em uma migração gradual, ela pode nascer com data para desaparecer. Em integrações permanentes com gateways, transportadoras ou outros fornecedores, tende a continuar como uma fronteira estável.
Se a fronteira se justifica, estes são os pontos que merecem uma decisão explícita:
Não use uma ACL apenas porque existe uma integração.
Use quando aceitar o modelo externo prejudicaria a linguagem, as regras ou a evolução da aplicação.
Antes de fechar, cinco perguntas rápidas sobre a fronteira e sobre quando ela compensa:
No contexto de uma ACL, o que significa "corrupção"?
Contratos, códigos e exceções externas raramente invadem uma aplicação de uma vez. Eles entram um if por vez, cada um pequeno demais para justificar uma camada inteira.
A Anti-Corruption Layer concentra esse conhecimento em uma fronteira controlada, para que cada lado continue falando sua própria linguagem.
Ela cobra por isso: novos tipos, adapters, traduções e testes.
Quando os modelos já dizem a mesma coisa, a camada pode apenas duplicar estrutura. E, quando fornecedores possuem capacidades realmente diferentes, ela não os torna intercambiáveis por mágica.
Uma boa integração permite que dois sistemas conversem.
Uma boa Anti-Corruption Layer permite que eles conversem sem obrigá-los a pensar da mesma forma.
Agora abra a integração mais antiga do seu projeto e procure um status, um DTO ou uma exceção do fornecedor fora da camada de integração.
Se encontrar, você achou uma fronteira que merece ser revista.
Guias e leituras sobre Anti-Corruption Layer, DDD e integração entre contextos.
Arquiteto e Engenheiro de Software | Java & IA