Quanto custa de verdade um serviço novo
O código é a parte barata. Esta é a fatura que vem junto com cada caixinha que você adiciona ao diagrama.
Um serviço novo no diagrama custa mais ou menos um dia para desenhar e uma semana para montar o esqueleto. Essa é a parte que todo mundo estima. A parte que ninguém estima é quanto ele custa nos cinco anos seguintes, e é esse número que quero tornar visível, porque muda decisões.
Não sou contra serviços. Sou contra serviços criados porque uma caixinha parecia mais limpa que um módulo, por gente que nunca viu a conta.
Os custos fixos
Todo serviço, independente do tamanho, carrega um piso de custo que um módulo dentro de uma unidade de deploy existente não carrega.
Ele precisa de um repositório ou entrada no workspace, um pipeline de CI, uma imagem de contêiner, um pipeline de deploy com pelo menos dois ambientes, um health check, um jeito de obter segredos, coletor de logs, exportador de métricas, propagação de contexto de trace, alertas, um dashboard e um runbook. Cada item são algumas horas na primeira vez e depois manutenção permanente: a imagem base envelhece, o runner de CI muda, a biblioteca de logging tem uma vulnerabilidade.
Ele precisa de uma identidade: conta de serviço, regras de rede, permissões para as coisas com que conversa. Alguém precisa decidir essas permissões e revisá-las quando mudam.
E precisa de dono. Um time com nome, uma escala de plantão que sabe que ele existe, um lugar no processo de incidentes. Serviços sem dono claro são os que ficam três anos num runtime sem patch.
Meu número aproximado: um serviço custa no mínimo de dois a quatro dias de engenheiro por trimestre só para existir, antes de qualquer feature. Dez serviços são um mês por trimestre de pura manutenção. Cinquenta são um engenheiro em tempo integral só para manter caixinhas vivas.
Os custos que crescem com as conexões
O custo fixo é o pequeno. O maior cresce com o número de coisas com que o serviço conversa.
Toda chamada entre serviços é uma chamada de rede que pode falhar, estourar o tempo ou devolver uma versão inesperada. Cada uma precisa de timeout, política de retry, decisão sobre circuit breaker, história de idempotência e teste de contrato. Uma chamada de função dentro de um monólito não precisa de nada disso.
Todo dado de que dois serviços precisam agora está duplicado, com um problema de sincronização, ou é buscado em tempo de execução, com um problema de latência e disponibilidade. Um join que era uma linha de SQL agora são duas chamadas e um merge em memória, ou um pipeline de eventos que mantém uma cópia atualizada.
Toda transação que cruza uma fronteira agora é uma saga, ou silenciosamente deixou de ser transação. Já auditei muitos sistemas em que uma operação que deveria ser atômica foi dividida em três serviços, e o modo de falha era dinheiro em estado inconsistente que um job noturno 'consertava'.
A depuração segue a mesma curva. Um stack trace vira um trace entre serviços, ótimo se o tracing está configurado em todo lugar e péssimo se um salto perde o contexto. A primeira hora de todo incidente vai para descobrir qual caixinha está falhando.
Os custos sobre as pessoas
Há um custo mais silencioso. Todo serviço é um lugar que um engenheiro novo precisa aprender a encontrar. É um repositório para clonar, um ambiente local para fazer funcionar, um dono para perguntar. O onboarding cresce mais com a quantidade de serviços do que com linhas de código.
E toda fronteira é um lugar onde alguém pode dizer 'não é meu serviço'. Arquiteturas com muitos serviços pequenos tendem a muitos pontos cegos, onde um bug mora na costura e nenhum lado é dono da costura.
Quando o custo vale a pena
A conta vale a pena quando o serviço compra algo que um módulo não consegue. Escala independente, quando uma parte do sistema tem de fato outro perfil de carga e você mediu isso. Deploys independentes, quando dois times se bloqueiam pelos releases um do outro e você contou os bloqueios. Isolamento, quando um componente é arriscado, está em outra linguagem ou precisa de sandbox. Uma fronteira real de propriedade, quando um time separado vai ser dono dele por anos.
Se nada disso se aplica, a escolha honesta é um módulo com interface clara dentro da unidade de deploy existente. Ele entrega a maior parte dos benefícios de design, a separação, a fronteira com nome, o contrato, por quase nenhum custo operacional. E quando um dos motivos acima se tornar verdade, o módulo já tem o formato certo para extração.
Então minha regra antes de adicionar uma caixinha: escreva a fatura. Custos fixos, custos de conexão, custos sobre pessoas, num parágrafo. Depois escreva o que a caixinha compra. Se o segundo parágrafo for mais curto que o primeiro, desenhe um módulo.