Como aplicar o padrão anti-corruption layer em integrações legadas
1. Entendendo o problema: corrupção de domínio em integrações legadas
1.1. O que é corrupção de domínio e como sistemas legados a causam
Corrupção de domínio ocorre quando conceitos, regras e estruturas de dados de um sistema legado contaminam o modelo de domínio de um novo sistema. Imagine que você está construindo uma plataforma moderna de e-commerce, mas precisa integrar com um sistema legado de estoque que usa campos como STATUS_ITEM com valores como "A", "I", "P". Se esses códigos misteriosos aparecerem no seu domínio limpo, você tem corrupção.
1.2. Sintomas comuns: vazamento de modelos, acoplamento indesejado e perda de integridade
Os sintomas são claros:
- Vazamento de modelos: classes do legado aparecem em repositórios do novo sistema
- Acoplamento indesejado: uma mudança no banco legado quebra sua aplicação moderna
- Perda de integridade: regras de negócio do legado são replicadas em vários lugares
1.3. Por que refatorar o legado nem sempre é viável: riscos e custos
Sistemas legados frequentemente não possuem testes, documentação desatualizada e dependências críticas. Refatorá-los pode custar milhões e parar operações. O padrão Anti-Corruption Layer (ACL) surge como alternativa pragmática.
2. Fundamentos do padrão Anti-Corruption Layer (ACL)
2.1. Definição formal do padrão (Domain-Driven Design) e seu propósito
Eric Evans, em Domain-Driven Design, define ACL como uma fronteira protetora que traduz o modelo do sistema legado para o modelo do novo domínio. Seu propósito é isolar o domínio limpo das complexidades e inconsistências externas.
2.2. Componentes essenciais: adaptadores, tradutores e fronteiras
Um ACL típico contém:
- Adaptadores: interfaces que encapsulam a comunicação com o legado
- Tradutores (mappers): convertem objetos legados em objetos de domínio
- Fronteiras: serviços que expõem operações do domínio sem vazar detalhes do legado
2.3. Diferenças entre ACL e outras camadas (facade, adapter, proxy)
Diferente de um Facade que apenas simplifica, o ACL traduz ativamente conceitos. Enquanto um Adapter adapta interfaces, o ACL adapta modelos. O Proxy controla acesso, mas o ACL controla significado.
3. Mapeando a fronteira entre o domínio novo e o sistema legado
3.1. Identificando os bounded contexts que precisam de proteção
Analise quais contextos do seu novo sistema dependem de dados ou serviços legados. Exemplo: contexto de "Pagamentos" que precisa consultar um sistema de cobrança legado.
3.2. Analisando contratos e modelos do sistema legado
Mapeie APIs, schemas de banco e mensagens. Documente inconsistências:
- Nomes confusos (ex: CLIENT_ID vs customerId)
- Tipos incompatíveis (ex: data como string "20230101")
- Valores mágicos (ex: status "9" = "cancelado")
3.3. Definindo o modelo de domínio “limpo” que o ACL deve expor
Crie interfaces que seu domínio deseja consumir:
// Modelo de domínio desejado
public class Cliente {
private String id;
private String nome;
private LocalDate dataCadastro;
private StatusCliente status;
}
public enum StatusCliente {
ATIVO, INATIVO, BLOQUEADO
}
4. Estruturando a implementação do ACL
4.1. Camada de tradução: mapeamento de objetos legados para objetos de domínio
// Tradutor que isola o domínio do modelo legado
public class ClienteLegadoMapper {
public Cliente paraDominio(ClienteLegado legado) {
return new Cliente(
legado.getCodCli(),
legado.getNomeCli(),
converterData(legado.getDtCad()),
converterStatus(legado.getStatusCli())
);
}
private LocalDate converterData(String dataString) {
// Exemplo: "20230101" -> LocalDate
return LocalDate.parse(dataString, DateTimeFormatter.BASIC_ISO_DATE);
}
private StatusCliente converterStatus(String status) {
// Mapeamento de códigos legados para enum do domínio
return switch (status) {
case "A" -> StatusCliente.ATIVO;
case "I" -> StatusCliente.INATIVO;
case "B" -> StatusCliente.BLOQUEADO;
default -> throw new IllegalArgumentException("Status desconhecido: " + status);
};
}
}
4.2. Camada de comunicação: encapsulamento de chamadas (REST, SOAP, filas, JDBC)
// Adaptador que encapsula a comunicação SOAP com o legado
public class ClienteLegadoAdapter {
private final SoapClient soapClient;
private final ClienteLegadoMapper mapper;
public Cliente buscarCliente(String id) {
try {
ClienteLegadoResponse response = soapClient.consultarCliente(id);
return mapper.paraDominio(response.getCliente());
} catch (SoapFaultException e) {
throw new LegadoIndisponivelException("Falha ao consultar legado", e);
}
}
}
4.3. Tratamento de erros, timeouts e inconsistências de dados legados
// Serviço ACL com tratamento robusto
public class ClienteAclService {
private static final int TIMEOUT_SEGUNDOS = 5;
private final ClienteLegadoAdapter adapter;
private final Cache<String, Cliente> cache;
public Optional<Cliente> buscarCliente(String id) {
// Tenta cache primeiro
Cliente cached = cache.getIfPresent(id);
if (cached != null) return Optional.of(cached);
try {
Cliente cliente = adapter.buscarCliente(id);
cache.put(id, cliente);
return Optional.of(cliente);
} catch (TimeoutException e) {
// Retorna dados do cache expirado como fallback
return Optional.ofNullable(cache.getIfPresent(id));
} catch (DadoInvalidoException e) {
// Loga e retorna vazio
Logger.warn("Dado inválido no legado para cliente {}", id, e);
return Optional.empty();
}
}
}
5. Estratégias de tradução e transformação de dados
5.1. Mapeamento simples (value objects) vs. transformação complexa (agregados)
Para value objects, use mapeamentos diretos. Para agregados, pode ser necessário múltiplas chamadas e composição:
// Transformação complexa: compondo agregado a partir de múltiplas fontes
public class PedidoCompletoMapper {
public Pedido paraDominio(PedidoLegado pedidoLegado, List<ItemLegado> itensLegados) {
List<ItemPedido> itens = itensLegados.stream()
.map(this::converterItem)
.collect(Collectors.toList());
return new Pedido(
pedidoLegado.getNumPedido(),
converterData(pedidoLegado.getDataPedido()),
itens,
calcularTotal(itens)
);
}
}
5.2. Lidando com dados anêmicos, tipos incompatíveis e nulos inesperados
// Tratamento defensivo para dados legados
public class TratamentoNulos {
public String safeString(String valor) {
return (valor == null || valor.trim().isEmpty()) ? "N/A" : valor.trim();
}
public BigDecimal safeDecimal(String valor) {
if (valor == null || valor.trim().isEmpty()) return BigDecimal.ZERO;
try {
return new BigDecimal(valor.replace(",", "."));
} catch (NumberFormatException e) {
return BigDecimal.ZERO;
}
}
}
5.3. Cache e materialização de consultas para reduzir acoplamento temporal
Implemente cache com TTL configurável e materialização de consultas frequentes em tabelas locais, sincronizadas periodicamente com o legado.
6. Testando e validando a camada de anticorrupção
6.1. Testes unitários para tradutores e mapeadores
// Teste unitário do mapper
public class ClienteLegadoMapperTest {
@Test
public void deveConverterStatusA_paraAtivo() {
ClienteLegado legado = new ClienteLegado("123", "João", "20230101", "A");
Cliente resultado = new ClienteLegadoMapper().paraDominio(legado);
assertEquals(StatusCliente.ATIVO, resultado.getStatus());
}
@Test(expected = IllegalArgumentException.class)
public void deveLancarExcecaoParaStatusInvalido() {
ClienteLegado legado = new ClienteLegado("123", "João", "20230101", "X");
new ClienteLegadoMapper().paraDominio(legado);
}
}
6.2. Testes de integração simulando o comportamento errático do legado
// Simulando timeouts e respostas inconsistentes
public class LegadoSimulator {
public static ClienteLegadoResponse simularTimeout() {
throw new TimeoutException("Simulação de timeout");
}
public static ClienteLegadoResponse simularDadoCorrompido() {
return new ClienteLegadoResponse("123", null, "data_invalida", "?");
}
}
6.3. Monitoramento: métricas de falhas, latência e integridade do ACL
Implemente métricas como:
- Taxa de sucesso de traduções
- Latência média das chamadas ao legado
- Número de fallbacks ativados
- Quantidade de dados inconsistentes detectados
7. Armadilhas comuns e boas práticas
7.1. Evitar o “ACL gigante”: separação por contexto e responsabilidade
Crie ACLs separados para cada bounded context. Um ACL de "Clientes" não deve lidar com "Produtos". Mantenha cada ACL focado em seu domínio.
7.2. Não replicar lógica de negócio legada dentro da camada
O ACL traduz dados, não replica regras. Se o legado tem uma regra "se status = A e data > 2020, então cliente premium", não coloque isso no ACL. Isso é lógica de negócio que pertence ao domínio.
7.3. Quando evoluir o ACL para um serviço autônomo ou substituir o legado
Considere extrair o ACL para um microsserviço independente quando:
- O legado é compartilhado entre múltiplos sistemas
- A complexidade de tradução cresce significativamente
- Você precisa escalar o ACL independentemente
Quando o custo de manter o ACL supera o de substituir o legado, é hora de planejar a migração.
Referências
- Anti-Corruption Layer - Martin Fowler — Artigo seminal de Martin Fowler explicando o padrão e seus casos de uso
- Domain-Driven Design: Tackling Complexity in the Heart of Software - Eric Evans — Livro que formalizou o padrão ACL no contexto de DDD
- Anti-Corruption Layer Pattern - Microsoft Azure Architecture Center — Documentação oficial da Microsoft com implementação prática do padrão
- Implementing Anti-Corruption Layer in Java - Baeldung — Tutorial técnico com exemplos de código Java para implementar ACL
- Anti-Corruption Layer: A Practical Guide - Thoughtworks — Guia prático da Thoughtworks com estratégias de implementação e armadilhas comuns
- Integrating with Legacy Systems Using ACL - InfoQ — Artigo do InfoQ sobre integração de sistemas legados usando ACL
- Patterns of Enterprise Application Architecture - Martin Fowler — Livro que complementa o ACL com outros padrões de integração