A armadilha da biblioteca compartilhada
O pacote criado para evitar duplicação costuma ser o acoplamento mais forte do sistema. Como reconhecer, e o que fazer no lugar.
Em quase toda base de código com vários serviços que audito existe um pacote chamado common, core, shared ou utils. Começou como uma boa ideia: três serviços precisavam da mesma formatação de data, do mesmo middleware de autenticação, do mesmo tipo de erro. Copiar três vezes parecia errado. Então alguém extraiu, publicou, e todo mundo passou a depender.
Dois anos depois, esse pacote é o motivo de nada ir para produção numa sexta-feira. Tem quarenta dependências. Contém os modelos do banco. Atualizá-lo em um serviço significa atualizar em doze, e um dos doze pertence a um time que não existe mais. A biblioteca criada para reduzir acoplamento virou o acoplamento mais forte de todo o sistema.
Como a armadilha se fecha
A armadilha tem uma forma previsível. Primeiro, o pacote compartilhado guarda só helpers puros: formatação, validação, tipos pequenos. Tudo bem. Depois alguém adiciona um cliente para uma API interna, com sua política de retry e sua configuração. Depois os modelos do ORM entram 'para ficar tudo num lugar só'. Depois um leitor de feature flags que faz uma chamada de rede na hora do import.
A cada passo o pacote fica mais útil e menos estável. Cada adição arrasta dependências e premissas sobre o ambiente: uma variável que precisa existir, um banco que precisa estar acessível, uma versão específica de framework. Os consumidores não pediram nada disso. Queriam o formatador de data.
O momento em que sei que a armadilha se fechou é quando um time trava o pacote compartilhado numa versão antiga e se recusa a atualizar. Agora existem duas versões em produção com comportamentos diferentes, e a 'fonte única da verdade' são duas verdades que ninguém compara.
Duplicação é mais barata que a abstração errada
O instinto de extrair vem de uma regra ensinada cedo: não se repita. É uma boa regra dentro de uma base com uma unidade de deploy. Entre serviços com deploys independentes, ela se inverte. Toda linha compartilhada é uma linha que precisa mudar em sincronia, e sincronia é exatamente o que os serviços deveriam evitar.
Meu limiar: se dois serviços têm código parecido, deixe. Com três, olhe de perto e pergunte se é igual por causa de um conceito compartilhado ou por coincidência. Formatação de data é coincidência; os dois poderiam mudar de forma independente e nada quebraria. Um tipo de dinheiro com moeda e regras de arredondamento é um conceito compartilhado; se ele divergir, as faturas discordam.
Mesmo para conceitos compartilhados, a coisa compartilhada muitas vezes deveria ser uma especificação, não um pacote. Um JSON schema, um documento OpenAPI, um arquivo protobuf. Cada serviço gera ou escreve a própria implementação a partir da especificação. A especificação é versionada e estável. As implementações são livres.
O que cabe num pacote compartilhado
Existe um pacote compartilhado que funciona, e ele é chato. Tem zero dependências de runtime, ou o mais perto disso que a linguagem permite. Não faz I/O. Não lê ambiente. Não tem opinião sobre qual framework você usa. É versionado semanticamente, e toda mudança incompatível vem com um codemod ou uma nota de migração clara.
Passam nesse teste: tipos puros, objetos de valor como Dinheiro ou Email, funções puras de validação, constantes que realmente são constantes, e algoritmos pequenos como um gerador de ID. Reprovam: clientes HTTP, modelos de banco, middleware de autenticação, configuração de logging, carregadores de config e qualquer coisa que conheça o nome de outro serviço.
Para o grupo reprovado, as alternativas são melhores do que parecem. Um cliente HTTP para uma API interna pertence ao time da API, gerado da especificação e publicado no ritmo dela. Middleware de autenticação pertence à borda, num gateway, não repetido em cada serviço. Configuração de logging são dez linhas; copie. Modelos de banco pertencem a exatamente um serviço, o dono da tabela, e todo o resto conversa com ele por API ou por evento.
Como sair
Se você já está na armadilha, não tente um big bang. Comece medindo: para cada export do pacote, conte os consumidores. Na minha experiência, um terço tem um único consumidor e pode voltar para ele hoje. Outro terço é puro e pode ficar. O último terço é o problema, e costuma ser o que faz I/O.
Para esses, escolha o mais doloroso, encontre o dono legítimo e mova para lá com um shim de descontinuação no pacote compartilhado que reexporta da casa nova. Remova o shim depois de dois ciclos de release. Repita. É lento, e tudo bem, porque a alternativa é um pacote em que ninguém ousa mexer.
O teste que uso no fim: qualquer serviço consegue atualizar o pacote compartilhado, sozinho, numa terça-feira, sem reunião? Se sim, a biblioteca é uma biblioteca. Se não, é um monólito distribuído com passos a mais.