Anti-corruption layer: protegendo domínios de sistemas legados

1. Introdução ao Conceito de Anti-corruption Layer (ACL)

1.1. Definição e origem no Domain-Driven Design (Eric Evans)

A Anti-corruption Layer (ACL) é um padrão arquitetural introduzido por Eric Evans em seu livro Domain-Driven Design: Tackling Complexity in the Heart of Software (2003). O conceito nasce da necessidade de proteger um modelo de domínio rico e bem definido contra a contaminação de conceitos, regras e estruturas de sistemas externos — especialmente sistemas legados que possuem modelos pobres, inconsistentes ou com semântica distorcida.

1.2. Problema central: corrupção do modelo de domínio por sistemas externos legados

Quando um sistema moderno precisa se integrar a um sistema legado, há uma tendência natural de "importar" os conceitos do legado para o novo domínio. Isso gera corrupção: entidades do domínio limpo passam a carregar atributos desnecessários, regras de negócio do legado migram para o novo sistema, e o modelo perde sua pureza e coesão. A ACL atua como uma barreira que traduz, filtra e adapta as comunicações entre os dois mundos.

1.3. Diferença entre ACL e outras estratégias de integração

Diferente do Shared Kernel, onde dois bounded contexts compartilham um subconjunto do modelo, a ACL mantém total separação. Já o Open Host Service expõe um serviço padronizado para consumo externo; a ACL, por outro lado, é uma camada que consome serviços externos e os adapta ao modelo interno. A ACL é essencialmente uma ponte defensiva, não uma interface pública.

2. Quando e Por Que Utilizar uma ACL

2.1. Cenários típicos

  • Sistemas legados monolíticos: bancos de dados com esquemas normalizados de forma inadequada, stored procedures com lógica embutida, APIs REST inconsistentes.
  • APIs de terceiros: provedores que mudam contratos sem aviso, modelos de dados genéricos demais.
  • Bancos de dados não normalizados: tabelas que misturam conceitos de diferentes domínios, campos com múltiplos significados.

2.2. Sinais de alerta

  • Código do domínio limpo contém if (legacyField == "Y") ou referências diretas a nomes de colunas do banco legado.
  • Alterações no sistema legado quebram funcionalidades no novo sistema sem relação aparente.
  • Testes de unidade precisam simular o comportamento do legado para validar regras de negócio.

2.3. Trade-offs

Implementar uma ACL exige investimento inicial em mapeamento, adaptadores e testes. O ganho é enorme em manutenibilidade: o domínio limpo evolui independentemente do legado, e a substituição gradual do sistema antigo se torna viável sem refatoração catastrófica.

3. Estrutura Típica de uma Anti-corruption Layer

3.1. Componentes principais

  • Adaptadores: traduzem chamadas entre interfaces (ex: REST do legado → interface do repositório do domínio).
  • Tradutores (Translators): convertem objetos de um modelo para outro (ex: LegacyCustomerDTOCustomer).
  • Fachadas (Facades): simplificam interações complexas com o legado, expondo apenas o necessário.
  • Serviços de integração: orquestram chamadas, tratam erros, implementam retry e circuit breaker.

3.2. Fluxo de dados

Sistema Legado → ACL (Adaptador → Tradutor → Fachada) → Domínio Limpo
Domínio Limpo → ACL (Fachada → Tradutor → Adaptador) → Sistema Legado

3.3. Separação entre modelos

O modelo legado (DTOs, entidades anêmicas) jamais deve vazar para o domínio limpo. A ACL é responsável por criar uma representação limpa e semanticamente adequada.

4. Estratégias de Implementação da ACL

4.1. Tradução de objetos

Mapeamento explícito entre DTOs legados e entidades de domínio, preferencialmente com testes que validem cada campo.

4.2. Fachada de serviço

Encapsula chamadas complexas (múltiplos endpoints, autenticação, paginação) em uma interface limpa.

4.3. Adaptador de repositório

Isola consultas e persistência, aplicando filtros e normalizações antes de expor dados ao domínio.

5. Exemplos Práticos de Código (em texto)

5.1. Exemplo 1: Tradução de um modelo de cliente legado para o domínio limpo

// Modelo legado (vindo de API externa)
class LegacyCustomerDTO {
    String id;
    String full_name;          // "SILVA, João"
    String status_code;        // "A" = ativo, "I" = inativo
    String last_purchase_date; // "20230115"
}

// Modelo de domínio limpo
class Customer {
    CustomerId id;
    FullName name;             // objeto valor com firstName, lastName
    boolean isActive;
    LocalDate lastPurchaseDate;
}

// Tradutor
class CustomerTranslator {
    Customer translate(LegacyCustomerDTO dto) {
        String[] parts = dto.full_name.split(", ");
        FullName name = new FullName(parts[1], parts[0]); // João Silva

        return new Customer(
            new CustomerId(dto.id),
            name,
            dto.status_code.equals("A"),
            LocalDate.parse(dto.last_purchase_date, DateTimeFormatter.BASIC_ISO_DATE)
        );
    }
}

5.2. Exemplo 2: Fachada que converte chamadas REST inconsistentes em eventos de domínio

// Fachada que consome API legada e produz eventos limpos
class LegacyOrderFacade {
    private final LegacyApiClient client;
    private final OrderTranslator translator;
    private final EventPublisher publisher;

    void fetchAndPublishOrders() {
        List<LegacyOrderDTO> rawOrders = client.getOrders("pending");
        for (LegacyOrderDTO raw : rawOrders) {
            OrderDomainEvent event = translator.toDomainEvent(raw);
            publisher.publish(event);
        }
    }
}

// Evento de domínio limpo
class OrderPlacedEvent {
    OrderId id;
    CustomerId customerId;
    Money total;
    List<OrderItem> items;
}

5.3. Exemplo 3: Adaptador de repositório que filtra e normaliza dados de um banco legado

// Adaptador de repositório
class LegacyCustomerRepositoryAdapter implements CustomerRepository {
    private final LegacyDatabase legacyDb;
    private final CustomerTranslator translator;

    @Override
    public Customer findById(CustomerId id) {
        LegacyCustomerRecord record = legacyDb.query(
            "SELECT * FROM clientes WHERE codigo = ?", id.value()
        );
        if (record == null) return null;

        // Filtra registros com status deletado
        if ("D".equals(record.status)) return null;

        return translator.translate(record);
    }

    @Override
    public void save(Customer customer) {
        // Converte para modelo legado e persiste
        LegacyCustomerRecord record = translator.toLegacy(customer);
        legacyDb.update("UPDATE clientes SET ... WHERE codigo = ?", record);
    }
}

6. Testabilidade e Evolução da ACL

6.1. Como testar a camada de tradução isoladamente

Testes de unidade para tradutores são diretos: crie DTOs legados com valores conhecidos e verifique se as entidades de domínio são produzidas corretamente. Testes de integração validam a fachada com mocks do sistema legado.

6.2. Estratégias de versionamento

A ACL deve versionar seus contratos internos. Quando o sistema legado muda, apenas a ACL é alterada — o domínio limpo permanece intacto. Use testes de regressão para garantir que a tradução continua correta.

6.3. Refatoração gradual

A ACL permite substituir o legado por partes: um novo microsserviço pode ser introduzido atrás da mesma interface da ACL, e o domínio limpo não percebe a mudança.

7. Relação com Outros Padrões de Integração

7.1. ACL vs. Open Host Service

Enquanto o Open Host Service expõe um serviço padronizado para ser consumido, a ACL consome serviços externos. Um sistema pode ter ambos: um Open Host Service para seus clientes e uma ACL para consumir o legado internamente.

7.2. ACL e Context Mapping

No mapeamento de contextos (Context Mapping), a ACL é representada como uma camada entre dois bounded contexts, geralmente entre um contexto core (limpo) e um contexto legado (suporte ou genérico).

7.3. ACL combinada com Event Storming

Durante sessões de Event Storming, eventos de fronteira entre sistemas podem ser identificados. Esses eventos são candidatos naturais a serem tratados pela ACL, que pode traduzi-los para eventos de domínio internos.

8. Considerações Finais e Boas Práticas

8.1. Armadilhas comuns

  • Superengenharia: criar uma ACL complexa para integrações simples que poderiam ser resolvidas com mapeamento direto.
  • Acoplamento excessivo à tradução: a ACL pode crescer e se tornar um monolito próprio. Mantenha-a enxuta.
  • Ignorar tratamento de erros: falhas no legado devem ser traduzidas para exceções do domínio, não propagadas como estão.

8.2. Recomendações

  • Mantenha a ACL focada apenas no necessário para proteger o domínio.
  • Documente cada mapeamento e decisão de tradução.
  • Estabeleça testes de contrato que garantam que a ACL ainda funciona quando o legado muda.

8.3. Relação com outros artigos da série

Este artigo se conecta diretamente a outros padrões de integração abordados na série: Shared Kernel (compartilhamento controlado), Context Mapping (visão macro das fronteiras) e Modularity Metrics (como medir o isolamento alcançado pela ACL).

Referências