Seu time quer publicar pacotes NuGet mas não quer manter um NuGet Server. Quer rodar containers mas não quer contratar um Container Registry. Quer processar mensagens assíncronas mas acha que uma tabela com coluna status no PostgreSQL resolve.
A pergunta que ninguém faz é: por que você está adotando uma solução cujo custo operacional mínimo você se recusa a pagar?
O problema não é a ferramenta — é o subterfúgio
Eu já perdi a conta de quantos projetos .NET eu avaliei onde a equipe adotou um conceito bom conceito, um bom padrão arquitetural, uma boa tecnologia, mas na hora de tornar real a implementação, optou por gambiarras que quebravam completamente o conceito original.
E os problemas são dos mais diversos, desde a :
- adoção de filas, mas em banco de dados.
- adoção de serviços de identidade, mas abstraindo o serviço
- adoção de cache, mas em banco ou em disco
- adoção de NuGet para publicar pacotes, mas compartilhando pacotes em diretórios
- adoção de docker ou kubernetes, mas sem querer adotar container registry
Os padrões se repetem com uma previsibilidade deprimente:
NuGet sem servidor — O time cria pacotes NuGet e distribui via pasta compartilhada, feed local, ou pior: copia DLLs entre repositórios. A consequência? Ninguém sabe qual versão está em produção. Não existe rastreabilidade. O pacote “compartilhado” vira um Frankenstein que cada projeto consome de um jeito.
Containers sem registry — O time escreve Dockerfiles, builda imagens localmente, e faz deploy via docker save e docker load, ou publica direto no host. Não existe versionamento de imagens. Não existe rollback confiável. O “container” virou um zip quase sofisticado.
Filas no banco de dados — O time precisa de processamento assíncrono, mas em vez de adotar RabbitMQ ou qualquer broker real, cria uma tabela queue_messages com polling a cada 5 segundos. O banco de dados, que já era o gargalo, agora também é o message broker. A fila não tem dead-letter, não tem retry com backoff, não tem observabilidade, não tem nada que uma fila de verdade oferece desde o primeiro segundo de operação.
Cada um desses cenários compartilha a mesma raiz: a recusa em assumir o custo operacional das decisões técnicas tomadas.
O custo invisível do atalho
Subterfúgios não são gratuitos. Eles parecem mais baratos no início porque transferem o custo do orçamento de infraestrutura para o orçamento de complexidade. E complexidade é a dívida mais cara que existe em software, porque ela cobra juros compostos em cada sprint.
Quando você evita um NuGet Server privado, você não economiza.
- Você paga com horas de debug tentando descobrir por que o projeto A usa a versão 2.1 do pacote e o projeto B usa a 2.3.
- Você paga com o time tentando descobrir um comportamento que só existe na biblioteca, que não reflete o que existe no repositório, e descobre que determinada versão foi gerada na máquina do dev, mas o respectivo código não foi corretamente versionado.
- Paga com builds quebrados porque alguém atualizou a DLL na pasta compartilhada sem avisar.
- Paga com a impossibilidade de fazer rollback de um componente compartilhado para uma versão anterior.
Quando você evita um Container Registry,
- você paga com deploys que não podem ser reproduzidos porque a imagem foi construída na máquina de alguém.
- Paga com zero auditoria sobre o que está rodando em produção.
- Paga com a incapacidade de escalar horizontalmente de forma confiável.
- Paga com a inviabilidade de usar serviços profissionais e gerenciados para lidar com seu workload.
Quando você substitui um message broker por uma tabela no banco,
- você paga com dead locks, contenção e degradação de performance no banco principal.
- Paga com mensagens perdidas silenciosamente quando o polling falha.
- Paga com um sistema de retry artesanal que nunca vai cobrir todos os edge cases que um broker dedicado já resolveu há décadas.
- Paga com uma infinidade de conexões atolando o banco de dados, tornando-o quase inoperante.
- Paga com a dependência.
A infraestrutura que você se recusa a pagar não desaparece. Ela reaparece como complexidade no seu dia-a-dia., na sua operação, mesmo que sequer tenha alcançado produção ainda.
Faça apenas uma vez, faça bem feito, faça de forma definitiva
As interpretações de ágil e o conceito de agilidade foram deturpados e usados pra criar justificativas pra a gambiarra.
Para o profissional ruim, fazer apenas “o necessário” é o suficiente para “funcionar”.
E assim se gastamos mais lidando com a gambiarra, do que fazendo do jeito certo uma só vez.
Se precisamos compartilhar código, criamos pacotes nuget.
Se precisamos criar pacotes NuGet, nós subimos na nossa infra ou contratamos como serviço um NuGet Server.
Se vamos trabalhar com containers, nós subimos na nossa infra ou contratamos como serviço um Container Registry.
Se precisamos de cache distribuído, nós adotamos Redis ou Dragonfly.
Se precisamos de filas, nós usamos RabbitMQ — não uma tabela no banco de dados.
Aí o zé gambiarra diz: “Mas isso adiciona complexidade operacional!” — Sim, adiciona.
E torço para que a escolha de um novo padrão, uma nova arquitetura ou uma nova tecnologia tenha sido feita por necessidade.
Pois não há adição de padrão, arquitetura ou tecnologia que não gere demanda adicional. Até porque é essa demanda adicional que, normalmente, é a solução para o problema que a tecnologia resolve.
A nova complexidade é explícita, documentada e gerenciável e principalmente: Conhecida. Não é novidade e não deveria ser surpresa. Ao ponto de facilmente poder ser adicionada a um requisito de vaga no LinkedIn.
Diferente da complexidade acidental que o subterfúgio adotado dentro de casa, ou melhor a gambiarra caseira criaria, que é invisível, imprevisível e cumulativa.
Mas adotar a ferramenta certa não significa aceitar burocracia desnecessária. Significa projetar o fluxo para que fazer o certo seja o caminho mais fácil.
Na prática: NuGet + git submodules = Zero Burocracia
Um dos cenários mais comuns é o desenvolvimento de componentes compartilhados via NuGet.
O fluxo ingênuo é:
- Alterar o código do componente
- Fazer commit e push para o repositório
- Esperar a pipeline rodar
- A pipeline publica no pacote no NuGet Server
- Atualizamos a referência no projeto consumidor
- Finalmente podemos testar o novo código.
4 passos adicionais entre codificar e testar uma mudança.
Isso definitivamente mata a produtividade e incentiva exatamente o tipo de atalho que queremos evitar.
A abordagem que eu uso combina git submodules com condicionais no .csproj para eliminar essa fricção sem abrir mão da governança:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<!-- Em modo Debug, usa o código-fonte via submodule -->
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
<ProjectReference Include="..\submodules\MeuComponente\src\MeuComponente.csproj" />
</ItemGroup>
<!-- Em modo Release, exige o pacote publicado -->
<ItemGroup Condition="'$(Configuration)' == 'Release'">
<PackageReference Include="MeuComponente" Version="3.2.1" />
</ItemGroup>
</Project>
O que esse setup garante:
Durante o desenvolvimento (Debug), o componente é referenciado como projeto via submodule. O desenvolvedor altera o código do componente, compila, testa e depura — tudo localmente, sem pipeline, sem espera, sem burocracia.
No build oficial (Release), o componente é consumido como pacote NuGet. Se a versão 3.2.1 não existir no NuGet Server, o build falha. Não existe deploy sem a versão correta publicada.
Essa dualidade é o que torna o fluxo sustentável. O desenvolvedor não é penalizado durante o desenvolvimento — o ciclo de feedback é instantâneo. A pipeline não aceita atalhos — o pacote precisa existir, versionado e publicado, para que o release funcione. A rastreabilidade é total — cada release aponta para uma versão específica de cada componente.
O git submodule funciona como um portal bidirecional: em debug, você enxerga e edita o código real. Em release, a referência de projeto simplesmente não existe — o build exige o pacote concreto.
Dica: Eu uso esse mesmo conceito para microsserviços…
O princípio por trás da prática
O padrão aqui não é sobre NuGet ou git submodules. É sobre um princípio de design de fluxo de trabalho:
Fazer a coisa certa deve ser o caminho de menor resistência.
Fazer a coisa errada deve ser impossível — ou pelo menos, visivelmente mais difícil.
Quando você projeta o fluxo de forma que o build de release só funciona com o pacote publicado, você não precisa de documento de processo, não precisa de checklist, não precisa de code review focado em “será que ele atualizou a versão?”. O sistema impõe a regra.
Quando você dá ao desenvolvedor um submodule para iterar localmente, você remove o incentivo para copiar DLLs, referenciar pacotes locais, ou qualquer outra gambiarra que surge quando o caminho correto é burocrático demais.
E para o resto da infraestrutura?
O mesmo princípio se aplica a cada decisão operacional.
Container Registry — Sim, tem custo. O GitHub Container Registry tem um tier gratuito generoso. O GitLab tem registry integrado. O Harbor é open-source e pode ser self-hosted. O Azure Container Registry tem um SKU básico. A desculpa de custo não sobrevive a 15 minutos de pesquisa.
Message Broker — O RabbitMQ roda em um container com um docker compose up. O CloudAMQP tem tier gratuito para ambientes de desenvolvimento. Não existe cenário onde uma tabela com polling é mais fácil de operar do que um broker que já nasce com retry, dead-letter, exchange routing e management UI.
Cache distribuído — O Redis também roda em um container. O Dragonfly é um drop-in replacement com melhor performance e menor consumo de memória. Ambos oferecem observabilidade nativa que qualquer implementação caseira levaria meses para atingir.
Nenhuma dessas soluções é gratuita em termos de operação. Mas a operação delas é conhecida, documentada e suportada por comunidades enormes. A operação da sua gambiarra é conhecida por uma pessoa — e essa pessoa vai sair da empresa em algum momento.
Conclusão
A próxima vez que alguém no seu time sugerir “a gente resolve isso com uma tabela no banco” ou “dá pra compartilhar o pacote por pasta”, faça uma pergunta simples:
Se a solução correta existe, é acessível e é documentada
— por que estamos inventando uma versão pior?
A resposta quase sempre é uma combinação de preguiça operacional com medo de assumir responsabilidade por infraestrutura. E essas duas coisas, em um time que se propõe a trabalhar com sistemas distribuídos, são incompatíveis com o resultado que se espera entregar.
Faça o que precisa ser feito.
Pague o preço da decisão que você tomou
ou não tome a decisão alguma.









0 comentários