Hoje estou em um projeto onde a gente tem trabalhado bastante na padronização e na criação de novos serviços. Em um refinamento técnico, no meio de uma discussão sobre repetição de código, alguém fez uma pergunta simples:
“Por que a gente ainda não tem uma biblioteca própria para isso?”
A pergunta ficou na cabeça porque ela tocava em um problema real. Em algum momento, a arquitetura cresce, os serviços se multiplicam e algumas coisas começam a aparecer em todo canto: logs, autenticação, autorização, clientes HTTP, integrações internas, tratamento de erro, observabilidade.
Quando isso acontece, a dúvida deixa de ser “será que dá para criar uma lib?” e passa a ser “quanto está custando não ter uma?”.
Antes de seguir, vale alinhar o que estou chamando de SDK aqui.
Um SDK, Software Development Kit, é um conjunto de bibliotecas, utilitários e padrões que ajudam os times a implementar funcionalidades recorrentes sem começar do zero em cada serviço. No nosso caso, a ideia era concentrar o que sempre se repetia e entregar isso de um jeito simples de instalar, versionar e evoluir.
Não era sobre criar uma biblioteca “bonita” por vaidade técnica. Era sobre reduzir atrito no dia a dia.
O cenário que motivou a criação
Nossa quantidade de microserviços começou a crescer rápido. Com isso, alguns problemas ficaram difíceis de ignorar.
Problemas identificados
-
Código repetido em vários serviços: muita coisa aparecia de novo em cada repositório: logs, autenticação, autorização, clientes para APIs internas e pequenos utilitários.
-
Copy and paste na criação de novos projetos: para iniciar um serviço, era comum buscar trechos em outro repositório e adaptar manualmente.
-
Manutenção espalhada: uma mudança simples em uma regra comum podia virar várias alterações em serviços diferentes.
-
Padrões parecidos, mas não iguais: cada serviço resolvia o mesmo problema com pequenas diferenças. No começo parece inofensivo. Depois vira ruído para manter, debugar e evoluir.
Impactos negativos
Na prática, isso gerava alguns efeitos bem concretos:
- mais tempo para subir um serviço novo;
- retrabalho em tarefas que já deveriam estar resolvidas;
- inconsistência entre aplicações;
- mais risco em mudanças transversais;
- onboarding mais lento para quem entrava no time.
A decisão e os benefícios do SDK
A partir daí, fez sentido criar um SDK interno: um repositório versionado com módulos reutilizáveis para os problemas que apareciam com frequência.
O ponto principal foi definir bem o que entraria nele. Um SDK interno não pode virar uma pasta compartilhada para qualquer coisa. Se tudo entra, ele deixa de ser uma base útil e vira acoplamento disfarçado.
Começamos pelo que tinha repetição clara e valor imediato:
- envio e formatação de logs;
- padrões de observabilidade;
- clientes para integrações internas;
- utilitários usados por mais de um serviço;
- contratos e tipos compartilhados quando fazia sentido.
Com isso, os ganhos começaram a aparecer de forma bem prática.
- Manutenção em um lugar só: uma correção no SDK pode ser entregue para todos os serviços por meio de uma nova versão.
- Novos serviços começam mais rápido: o time não precisa remontar a mesma base toda vez.
- Padrões ficam mais fáceis de seguir: logs, métricas e integrações passam a ter uma forma recomendada de uso.
- Mudanças transversais ficam menos dolorosas: em vez de abrir vários PRs com código quase igual, a evolução acontece primeiro no pacote.
Distribuição e versionamento
Para distribuir o SDK internamente, usamos o Azure Artifacts como repositório privado de pacotes. Isso deixou a instalação parecida com qualquer outra dependência do ecossistema Node.js.
A estratégia ficou assim:
- cada release do SDK é publicada no Azure Artifacts;
- os serviços instalam a dependência via
npm install; - o versionamento permite controlar quando cada serviço atualiza;
- o pipeline de CI/CD publica novas versões sem depender de processo manual.
No package.json de um serviço, o uso fica simples:
{
"dependencies": {
"@org/opentrips-sdk": "^1.3.0"
}
}
Desafios encontrados
Criar um SDK interno ajuda bastante. Ao mesmo tempo, ele traz responsabilidades novas. Algumas delas apareceram logo no começo.
Principais obstáculos
-
Generalizar sem engessar: o SDK precisa atender casos reais, mas não pode tentar prever todos os cenários possíveis. Quando a abstração tenta ser esperta demais, ela atrapalha.
-
Cuidar das dependências: uma dependência problemática no SDK pode impactar vários serviços. Atualização, compatibilidade e segurança passam a exigir mais atenção.
-
Lidar com mais de uma stack: hoje usamos principalmente Node.js e Java, mas o SDK nasceu para Node.js. Isso levanta uma discussão importante: criar SDKs equivalentes para outras linguagens ou reforçar a padronização da stack?
-
Evitar dependência excessiva: o SDK deve facilitar a vida dos serviços, não impedir que eles evoluam. Se uma alteração simples exige mexer no SDK toda hora, talvez a fronteira esteja errada.
Exemplo prático de uso
Um dos primeiros módulos que criamos foi o cliente de logs. A ideia era esconder detalhes de envio, formato e tratamento de resposta, deixando o uso simples dentro dos serviços.
import { createLogsClient } from 'opentrips-sdk/logs';
// Criar cliente de logs
const logs = createLogsClient({
host: 'http://localhost:8080',
// apiKey: process.env.LOGS_API_KEY // Opcional
});
// Enviar log
const result = await logs.createLog({
provider: 'clickbus',
type: 'response',
payload: '<?xml version="1.0" encoding="UTF-8"?><empresa>...</empresa>',
correlationId: '038473a4-db4a-405a-b759-e52d035b3a0d',
operation: 'reserva',
resource: 'rodoviario',
extra: { teste: 'teste' }
});
if (result.ok) {
console.log('Log enviado com sucesso');
} else {
console.error('Erro ao enviar log:', result.error);
}
Arquitetura e estrutura do projeto
Organizamos o SDK em módulos independentes. Cada módulo tem sua própria documentação, exemplos e testes. Isso ajuda a manter uma fronteira clara entre as responsabilidades.
src/
├── index.ts # Ponto de entrada principal
├── logs/ # Módulo de Logs
│ ├── README.md
│ ├── example.ts
│ ├── index.ts
│ ├── client.ts
│ ├── http.ts
│ ├── types.ts
│ ├── utils.ts
│ └── __tests__/
├── observability/ # Módulo de Observabilidade
│ ├── README.md
│ ├── index.ts
│ ├── client.ts
│ ├── middleware.ts
│ ├── metrics-endpoint.ts
│ └── types.ts
└── [novos-módulos]/ # Futuros módulos...
Conclusão
Um SDK interno começa a fazer sentido quando a repetição deixou de ser só incômodo e virou custo real.
Para mim, alguns sinais são bons indicadores:
- vários serviços repetem a mesma implementação;
- mudanças comuns precisam ser aplicadas em muitos repositórios;
- novos projetos começam com muito código copiado;
- padrões importantes, como logs e observabilidade, variam demais entre serviços;
- o time perde tempo resolvendo problemas que já deveriam estar encapsulados.
Também é importante lembrar que SDK interno não é solução mágica. Ele precisa de dono, versionamento, documentação e critério para aceitar novos módulos. Sem isso, o problema só muda de lugar.
No nosso caso, a decisão fez sentido porque a duplicação já estava cobrando caro. Centralizar as partes certas ajudou a reduzir retrabalho, melhorar a consistência entre serviços e deixar o caminho mais curto para novos projetos.