O parceiro matou a API legada e te deu 30 dias. E agora?

Imagina que é uma manhã normal. Café na mão, você abre o e-mail e tem uma mensagem do time de desenvolvedores de um grande marketplace de delivery — aquele por onde passa boa parte do faturamento dos seus clientes. O assunto já dá um frio na barriga:

“A partir de 30 de junho, a API legada será descontinuada. A migração para a nova API é obrigatória em todos os módulos — autenticação, produtos, pedidos. Integradoras que não concluírem a migração terão a operação dos lojistas interrompida.”

Traduzindo do corporativês: migre em 30 dias ou seus clientes param de vender. E não é um módulo, é tudo — login, catálogo, recebimento de pedido. O coração da integração.

Se você nunca passou por isso, parabéns, aproveite. Quem vive de integrar com terceiros sabe que esse e-mail chega. Sempre chega. A pergunta nunca é “se”, é “quando” — e o quanto a sua arquitetura vai te ajudar ou te afundar quando ele chegar.

Esse artigo é sobre o segundo cenário virar o primeiro.

Por que isso dói tanto (e por que a culpa às vezes é nossa)

O motivo de uma deprecation dessas virar pesadelo quase nunca é a API nova ser difícil. É que a API antiga vazou pra dentro do seu sistema.

Sabe aquele código que foi escrito com pressa lá no começo? O Http::post('https://parceiro.com/v1/orders', $dados) espalhado em controller, em job, em command, em service. O json_decode do response do parceiro sendo lido direto na regra de negócio, com $response['order']['items'][0]['unit_price'] enfiado no meio do cálculo do pedido.

Quando o formato do parceiro está espalhado em 40 arquivos, trocar de API não é uma migração. É uma caçada. Você vai passar 30 dias com grep procurando todo lugar que toca a API antiga, rezando pra não esquecer nenhum — e vai esquecer.

A boa notícia: dá pra evitar isso. E mesmo que você já esteja no buraco, dá pra sair dele de um jeito controlado.

A defesa: Anti-Corruption Layer

O conceito vem do DDD e tem um nome dramático: Anti-Corruption Layer (ACL). A ideia, porém, é simples e quase óbvia quando você vê: o seu domínio não fala o idioma do parceiro. Ele fala o seu idioma. No meio, tem uma camada de tradução.

Na prática, isso começa com uma interface que descreve o que o seu sistema precisa — não o que o parceiro oferece:

interface CanalDeDelivery
{
    public function autenticar(Loja $loja): TokenDeAcesso;

    /** @return Pedido[] */
    public function buscarPedidosPendentes(Loja $loja): array;

    public function confirmarPedido(Pedido $pedido): void;
}

Repara que aqui não tem array, não tem JSON, não tem header HTTP, não tem nome de campo do parceiro. Tem Pedido, Loja, TokenDeAcesso — objetos do seu domínio. O resto do sistema só conhece essa interface. Ele não sabe (nem quer saber) se por baixo é a API v1, a v2 ou um pombo-correio.

O Adapter: onde a tradução acontece

Cada versão da API do parceiro vira um Adapter que implementa essa interface. A API legada:

final class CanalDeliveryServicesApi implements CanalDeDelivery
{
    public function __construct(private readonly ClienteHttp $http) {}

    public function buscarPedidosPendentes(Loja $loja): array
    {
        $resposta = $this->http->get("/v1/orders", [
            'merchant_id' => $loja->idExterno(),
            'status' => 'PENDING',
        ]);

        // A bagunça do formato do parceiro MORRE aqui dentro.
        return array_map(
            fn (array $cru) => Pedido::deDeliveryLegado($cru),
            $resposta['orders'] ?? []
        );
    }

    // autenticar(), confirmarPedido()...
}

E a API nova, que mudou rota, nomes de campo e estrutura:

final class CanalDeliveryMerchantApi implements CanalDeDelivery
{
    public function __construct(private readonly ClienteHttp $http) {}

    public function buscarPedidosPendentes(Loja $loja): array
    {
        // Rota nova, paginação nova, nomes de campo novos.
        $resposta = $this->http->get("/order/v2.0/merchants/{$loja->idExterno()}/orders");

        return array_map(
            fn (array $cru) => Pedido::deDeliveryMerchant($cru),
            $resposta['data'] ?? []
        );
    }

    // autenticar(), confirmarPedido()...
}

Olha o que aconteceu: a diferença entre as duas APIs ficou confinada em dois arquivos. O unit_price que virou price.amount, a rota que mudou, o status que antes era PENDING e agora é um enum diferente — tudo isso é problema do adapter, e só dele. O cálculo do pedido, o job que processa, o controller… nada disso muda uma linha. Eles continuam falando com CanalDeDelivery.

Esse é o pulo do gato: a deprecation deixou de ser uma caçada de 40 arquivos e virou a escrita de uma classe nova.

A virada de chave: feature flag por loja

Agora a parte que separa quem migra com sono tranquilo de quem migra no susto. Você não vira a chave de todos os clientes de uma vez na meia-noite do dia 30. Isso é pedir pra ter um incidente em massa sem ter como diagnosticar.

Você usa uma feature flag — de preferência por loja — pra escolher qual adapter entregar:

final class CanalDeliveryFactory
{
    public function __construct(
        private readonly FeatureFlags $flags,
        private readonly CanalDeliveryServicesApi $legado,
        private readonly CanalDeliveryMerchantApi $novo,
    ) {}

    public function para(Loja $loja): CanalDeDelivery
    {
        return $this->flags->habilitada('merchant_api', $loja)
            ? $this->novo
            : $this->legado;
    }
}

Com isso você migra uma loja, observa por um dia, migra dez, observa, migra cem. Se algo der errado, o rollback é mudar uma flag — não é um deploy de madrugada com o coração na mão. Quando chegar o dia 30, a esmagadora maioria já vai estar na API nova, testada em produção, e você dorme.

Shadow traffic: testando a API nova com dados reais antes de confiar nela

Tem um truque a mais, pra quem quer ir além. Antes de confiar na API nova pra valer, você pode rodar as duas em paralelo: a antiga responde de verdade, e a nova roda “na sombra”, só pra você comparar os resultados e logar as divergências.

final class CanalDeliveryComSombra implements CanalDeDelivery
{
    public function __construct(
        private readonly CanalDeDelivery $principal, // o legado, que vale
        private readonly CanalDeDelivery $sombra,    // o novo, em teste
        private readonly Logger $log,
    ) {}

    public function buscarPedidosPendentes(Loja $loja): array
    {
        $oficial = $this->principal->buscarPedidosPendentes($loja);

        try {
            $teste = $this->sombra->buscarPedidosPendentes($loja);
            if (count($teste) !== count($oficial)) {
                $this->log->warning('Divergência na Merchant API', [
                    'loja' => $loja->idExterno(),
                    'oficial' => count($oficial),
                    'sombra' => count($teste),
                ]);
            }
        } catch (\Throwable $e) {
            // A sombra NUNCA pode derrubar o fluxo real.
            $this->log->error('Sombra falhou', ['erro' => $e->getMessage()]);
        }

        return $oficial; // o cliente sempre recebe o resultado confiável
    }
}

Em uma ou duas semanas de sombra você descobre, com tráfego real, todos os cantos onde a API nova se comporta diferente — sem nenhum cliente ser afetado. Quando você finalmente vira a flag, já não tem surpresa.

Não esqueça da idempotência

Um detalhe que morde muita gente em migração de integração de pedido e pagamento: confirmar o mesmo pedido duas vezes durante a transição (porque você processou no fluxo antigo e reprocessou no novo) pode gerar duplicidade. Sempre que a operação não for naturalmente idempotente, mande uma chave de idempotência — quase toda API séria de pagamento/pedido suporta isso:

$this->http->post("/order/v2.0/orders/{$pedido->id()}/confirm", [
    'headers' => ['Idempotency-Key' => $pedido->chaveIdempotencia()],
]);

Reenviar com a mesma chave é seguro: o parceiro reconhece e não processa duas vezes.

O impacto técnico, sem hype

Nada disso é tecnologia nova. ACL, Adapter, feature flag e shadow traffic são padrões com anos de estrada. O ponto do artigo não é a novidade — é quando você paga esse custo.

Se você isola a integração atrás de uma interface desde o dia zero, a deprecation do parceiro vira um ticket de uma semana: escrever um adapter, rodar na sombra, virar flag loja a loja. Se você não isola, o mesmo e-mail vira 30 dias de caçada, deploy de madrugada e dedo cruzado.

A diferença entre os dois mundos é literalmente uma interface e um pouco de disciplina lá no começo, quando ninguém estava te cobrando isso ainda. É o tipo de decisão que não aparece em nenhuma sprint, mas que define se você vai surfar ou se afogar quando o parceiro mudar as regras — e ele vai mudar.

O e-mail vai chegar. Pode ser delivery, pode ser meio de pagamento, pode ser o ERP, pode ser o gateway de nota fiscal. Algum parceiro vai descontinuar uma API que está no caminho crítico do seu faturamento e vai te dar um prazo apertado.

A pergunta que importa não é “como eu migro rápido”. É “o formato desse parceiro está vazado pra dentro do meu domínio ou trancado num adapter?”. Se estiver trancado, você respira fundo, escreve uma classe, roda na sombra e vira uma flag. Se estiver vazado… bom, melhor começar a trancar hoje, com calma, antes do próximo e-mail.

Sua regra de negócio merece falar o seu idioma. Deixa o idioma do parceiro do lado de fora do muro.