04 - Anti-Corruption Layer (ACL)
video: https://youtu.be/... no frontmatter da nota.Anti-Corruption Layer (ACL)
A Anti-Corruption Layer é uma camada de tradução colocada entre o seu modelo de domínio e o modelo de um sistema externo, com a função explícita de impedir que conceitos estranhos contaminem o domínio próprio. Ela funciona como fronteira linguística: o lado interno fala a linguagem ubíqua do seu contexto, o lado externo fala o protocolo e o vocabulário do sistema integrado, e a camada faz a tradução bidirecional em um único lugar. Em produção, a ACL é a estratégia padrão para integrar sistemas legados, produtos de terceiros com modelos estranhos e qualquer upstream cujo vocabulário não se deseja adotar. O benefício é a soberania do modelo: o domínio evolui conforme o negócio exige, sem pedir licença às distorções alheias. O custo é permanente: toda mudança no sistema externo exige atualização da tradução, e a camada precisa ser mantida com a mesma seriedade do domínio que protege. Entender quando o preço vale a proteção é o que separa uma ACL saudável de uma burocracia de mapeamento.
Conceito e propósito
O nome descreve a função com precisão. Corrupção, aqui, é a invasão de conceitos externos ao modelo do contexto, e ela raramente chega de forma óbvia. Ela chega em formato de campo chamado cust_type no meio do payload, de estado cujo significado real é descoberto por tentativa e erro, de entidade que amarra três conceitos diferentes no mesmo registro. Sem proteção, esses conceitos se infiltram: uma classe do domínio passa a ter um atributo legado, depois uma regra que consulta esse atributo, depois uma migração que o propaga para novas tabelas. Dois anos depois, ninguém sabe distinguir o que é domínio e o que é fóssil de integração.
A ACL inverte a direção da dependência conceitual. O sistema externo não define o que o seu domínio significa; ele apenas fornece dados e operações, e a tradução decide como cada um deles é representado internamente. O domínio nunca importa tipos do cliente externo, nunca grava estruturas cruas do payload e nunca consulta estados estrangeiros em suas regras. Tudo isso é responsabilidade exclusiva da camada.
Vale notar que o adversário da camada não é tecnologia, e sim vocabulário. Duas equipes podem descrever o mesmo fato comercial com conjuntos de conceitos diferentes, e nenhuma das descrições está errada; elas são modelos diferentes. A ACL existe porque os dois modelos precisam coexistir sem que o maior e mais antigo enterre o menor e mais novo. Nesse sentido, a camada é uma decisão de linguística aplicada antes de ser uma decisão de código.
O problema que resolve
O cenário clássico é a integração com o sistema de retaguarda que administra o negócio há quinze anos. O sistema legado modela cliente como cadastro único, com tipo de conta em código numérico, status com valores que misturam situação fiscal e preferência de contato, e identificadores que reaproveita após exclusão. Seu contexto novo de fidelidade precisa de um conceito de Membro com regras próprias, e a pergunta é como consumir o legado sem virar legado.
Sem ACL, as três evoluções típicas de corrupção se desenrolam em sequência previsível. Primeiro, o modelo local ganha o campo tipo_conta como int, porque era assim que vinha do legado. Depois, regras de negócio passam a comparar esse número diretamente, espalhando o significado mágico pelo código. Por fim, a equipe descobre que o legado reaproveita identificadores, e correções pontuais começam a surgir em cada canto que confiou na unicidade. Com ACL, cada uma dessas peculiaridades é absorvida em um lugar: o tradutor converte tipo_conta em um conceito local com semântica clara, valida a suposição de unicidade na fronteira e documenta a regra do legado em um único arquivo de tradução.
Anatomia: fachada, adaptador e tradutor
A composição clássica da ACL tem três papéis, cada um com responsabilidade distinta. A fachada apresenta uma interface simples e conceitual para o serviço externo, no vocabulário do contexto consumidor. O adaptador implementa essa fachada usando a tecnologia real do upstream, ou seja, sabe fazer chamadas HTTP, consultas ou invocações de fila. O tradutor converte os dados entre os dois mundos, e é o único lugar onde os dois vocabulários coexistem.
# ACL do contexto de fidelidade protegendo-se do sistema legado de cadastro
class FachadaCadastro: # interface no vocabulário local
def obter_membro(self, id_membro: str) -> Membro: ...
class AdaptadorCadastroLegado(FachadaCadastro):
def __init__(self, cliente_legado, tradutor: TradutorMembro):
self.cliente = cliente_legado
self.tradutor = tradutor
def obter_membro(self, id_membro: str) -> Membro:
registro = self.cliente.buscar_por_id(int(id_membro))
if registro is None:
raise MembroInexistente(id_membro) # erro local, não erro do legado
return self.tradutor.para_membro(registro) # conversão única e explícita
class TradutorMembro:
MAPA_TIPO_CONTA = {1: Categoria.PF_ATIVO, 2: Categoria.PJ_ATIVO, 3: Categoria.INATIVO}
def para_membro(self, registro_legado) -> Membro:
categoria = self.MAPA_TIPO_CONTA.get(registro_legado.cust_type)
if categoria is None:
# código desconhecido do legado não entra no domínio como número mágico
raise TraducaoIndefinida("cust_type", registro_legado.cust_type)
return Membro(
id=IdentidadeMembro(registro_legado.codigo),
nome=registro_legado.full_name.strip() or None,
categoria=categoria,
)
O detalhe que separa uma ACL real de um simples wrapper é o tratamento de tradução indefinida. Quando o legado traz um código que o mapa não conhece, a camada não repassa o número cru para dentro do domínio, nem escolhe um default silencioso; ela sinaliza a falha de tradução, com log e métrica, e a equipe decide o significado do conceito novo. Esse comportamento transforma cada evolução do upstream em evento visível, e não em bug descoberto por conciliação mensal.
Tradução explícita e semântica preservada
A tradução de uma ACL não é renomeação de campos, é reconstrução de significado. Diferenças semânticas comuns exigem decisão documentada: identificadores reaproveitados pelo legado precisam de tratamento que garanta unicidade percebida pelo domínio; estados que misturam conceitos no legado precisam ser decompostos em conceitos locais separados; valores de dinheiro em precisão diferente precisam de conversão em um único ponto.
Boas práticas de tradução, com base em integrações reais:
- Toda conversão passa por função de tradutor testável, nunca por espalhamento de mapeamento pelo código do domínio.
- Tradução parcial é proibida: ou o registro entra completo e coerente no domínio, ou não entra, porque dado meio traduzido é dado corrupto com aparência saudável.
- Valores desconhecidos geram evento de tradução indefinida, com alerta, em vez de default silencioso que finge coerência.
- O mapa de tradução fica versionado junto do código, com comentário explicando a regra do legado, que é o conhecimento mais frágil da integração.
- Testes da ACL usam payloads reais capturados em produção como casos de regressão, cobrindo as peculiaridades que só o legado produz.
Integração com sistemas legados
A ACL é a estratégia natural para legado, e a razão é a assimetria de poder. O sistema antigo não vai se adaptar, não tem roadmap para atender você e não roda seus testes. A proteção precisa, portanto, viver toda no lado consumidor, e é exatamente o desenho da ACL.
Em projetos de modernização, a ACL combina com o padrão strangler fig. O novo contexto se conecta ao legado apenas pela camada anticorrupção, que inicialmente traduz chamadas diretas; com o tempo, as capacidades do legado são reimplementadas atrás da fachada, e os consumidores não mudam nada, porque a interface local permanece. A fachada que nasceu como tradução termina como substituição, e esse caminho gradual é o que permite modernizar sem grande projeto de corte.
O inventário de peculiaridades é o ativo mais valioso dessa integração. Cada surpresa do legado, reaproveitamento de id, código de tipo sem documentação, registro com vírgula no nome quebrando o parser, deve virar teste na suíte da ACL. A suíte cresce como uma coleção de fósseis documentados, e a memória da integração passa a viver no código, e não na cabeça de um especialista que pode sair da empresa.
O canal assíncrono recebe a mesma proteção, com ajuste de forma. Eventos publicados por um sistema externo chegam com schema próprio, e a ACL do consumidor traduz cada evento para o formato de domínio antes de qualquer processamento. Os mesmos princípios valem: nenhum evento cru avança, valores desconhecidos geram alerta, e a suíte de regressão guarda exemplos reais. Esse desenho evita o erro comum de espalhar conversões de payload entre os handlers, que é o equivalente assíncrono do mapeamento espalhado pelo código.
Custo de manutenção e trade-offs
| Aspecto | Efeito na produção |
|---|---|
| Custo inicial | Alto: desenhar fachada, adaptador, tradutor e suíte de tradução |
| Custo recorrente | Cada mudança relevante do upstream exige atualização e teste da camada |
| Custo de aprendizado | Time precisa dominar dois modelos, o local e o externo, ao menos na fronteira |
| Latência e complexidade | Adiciona um salto de código por chamada, irrelevante na maioria dos casos |
| Isolamento de falha | Erros do upstream aparecem como falha de tradução, e não como bug de domínio |
| Velocidade de evolução do domínio | Alta, porque o modelo local muda sem coordenação com o upstream |
| Risco de drift | Baixo, desde que a tradução seja o único ponto de contato com o externo |
O trade-off central é o mesmo de qualquer seguro: você paga prêmio contínuo para evitar perda potencial grande. A perda evitada é a corrupção do modelo, cujo custo real aparece em integrações que ficaram anos sem proteção e hoje precisam de meses de trabalho para separar domínio de fóssil. A regra de decisão pragmática: quanto mais distante e instável o upstream em relação ao seu domínio, mais a ACL se paga; quanto mais estável e próximo o modelo upstream, mais a tradução parece burocracia, e o Conformist ou o Shared Kernel tendem a ser a resposta melhor.
| Critério de escolha | Favorece ACL | Favorece Conformist |
|---|---|---|
| Estabilidade do modelo upstream | Instável ou em evolução imprevisível | Estável há anos |
| Qualidade semântica do modelo externo | Conceitos confusos ou misturados | Conceitos claros e adotáveis |
| Importância do domínio integrado | Diferenciador de negócio | Capacidade de suporte |
| Horizonte da integração | Longo prazo, troca de provedor prevista | Curto prazo ou fornecedor já consolidado |
| Poder de negociação com o upstream | Nenhum, sistema alheio | Alto, relação interna saudável |
Aplicação prática: passo a passo
A construção de uma ACL que sobrevive ao uso segue uma sequência estabelecida:
- Delimite a fronteira: escolha o conjunto mínimo de operações e dados do upstream que o contexto realmente usa, e resista a traduzir o sistema inteiro.
- Escreva a fachada no vocabulário local, antes de olhar o formato externo, para que a interface seja guiada pelo domínio e não pelo payload.
- Implemente o adaptador com a tecnologia real do upstream, isolando SDK, autenticação e protocolo dentro dele.
- Escreva o tradutor com o inventário de peculiaridades coletado, incluindo o tratamento explícito de valores desconhecidos.
- Monte a suíte de tradução com payloads reais capturados, incluindo os casos grotescos que só aparecem em produção.
- Proíba, por revisão de código, qualquer import do cliente externo fora da camada, com verificação automatizada quando possível.
- Instrumente traduções indefinidas como métrica e alerta, porque elas são o radar de evolução do upstream.
A ordem do passo 2 é a mais desrespeitada e a mais importante. Escrever a fachada olhando primeiro o payload do upstream produz uma camada que espelha o estrangeiro com nomes trocados, o pior dos dois mundos: o custo da tradução sem o benefício do modelo próprio. A fachada guiada pelo domínio, em contrapartida, funciona como especificação do que o contexto precisa, e o adaptador é preenchido depois para atender essa especificação. Essa inversão de direção é o que impede a ACL de degenerar em wrapper de conveniência.
Erros comuns e boas práticas
| Sintoma em produção | Causa raiz | Mitigação |
|---|---|---|
| Conceito do legado aparece em regra de domínio | Tradução incompleta, dado cru vazando para dentro | Tradução total na fronteira, sem exceção |
| Bug de integração corrigido em vários pontos | Mapeamento espalhado pelo código em vez de concentrado | Consolidar no tradutor e auditar imports |
| Upstream muda e ninguém percebe até a reclamação | Sem tratamento de tradução indefinida | Alerta de falha de tradução com triagem |
| ACL vira monolito de utilidades | Fronteira mal delimitada, traduzindo tudo | Reduzir ao conjunto mínimo de operações usadas |
| Dados divergentes entre domínio e upstream | Tradução parcial aplicada em caminhos diferentes | Tradutor único testado com payloads reais |
| Conhecimento do legado preso em uma pessoa | Peculiaridades sem teste nem comentário | Suíte de regressão como memória da integração |
A boa prática estruturante é tratar a ACL como módulo de primeira classe, com dono, testes e pipeline, e não como pasta de utilidades. A camada tem a responsabilidade mais delicada da integração, que é absorver a instabilidade alheia, e merece o mesmo rigor de engenharia do domínio que protege. A segunda prática é a revisão periódica do mapa de tradução contra o sistema externo; provedores depreciam campos silenciosamente, e uma revisão trimestral comparando o payload real com o mapa captura essas mudanças antes do incidente. A terceira é resistir à tentação de pular a camada em casos urgentes; o import direto de emergência, justificado pelo prazo, é o primeiro furo da fronteira, e furos de fronteira não se fecham sozinhos.