Monolith ou microservices: como escolher corretamente
1. Visão geral
A pergunta “devo escolher monolith ou microservices” praticamente aparece em toda discussão de design de sistema. Este artigo não oferece uma resposta única para todos, mas percorre critérios específicos: tamanho da equipe, velocidade de release, complexidade operacional e custo de comunicação entre serviços. Depois de ler, você terá um checklist para autoavaliar onde seu sistema deve estar no “espectro” monolith – monolith modular – microservices.
2. Monolith: simples mas frequentemente subestimado
Monolith significa que todo o negócio (Order, Customer, Payment, Inventory…) roda em um único processo, uma única base de código, um único deploy.
@RestController
@RequestMapping("/orders")
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping
public ResponseEntity<OrderResponse> createOrder(@RequestBody OrderRequest request) {
Order order = orderService.create(request);
return ResponseEntity.ok(OrderResponse.from(order));
}
}
@Service
public class OrderService {
private final InventoryService inventoryService;
private final PaymentService paymentService;
public OrderService(InventoryService inventoryService, PaymentService paymentService) {
this.inventoryService = inventoryService;
this.paymentService = paymentService;
}
public Order create(OrderRequest request) {
inventoryService.reserve(request.productId(), request.quantity());
paymentService.charge(request.customerId(), request.amount());
return new Order(request); // assume que o constructor salva no BD
}
}
OrderService chama diretamente InventoryService e PaymentService via chamada de método na mesma JVM. Não há rede, não há serialização, a transação pode ser encapsulada em um único @Transactional. Esta é a maior vantagem do monolith: a consistência é fácil de garantir, debug é simples, fazer deploy uma vez e pronto.
As desvantagens aparecem quando a equipe cresce: muitas pessoas mexem na mesma base de código, o tempo de build aumenta, o risco de deploy de um módulo pequeno acabar requiring rebuild de toda a aplicação.
Nota: Monolith não é sinônimo de ‘código bagunçado’. Um monolith bem escrito com módulos claros (packages por domínio, sem dependências cruzadas) ainda funciona bem para sistemas com milhões de usuários.
3. Microservices: separar para escalar de forma independente, em troca de maior complexidade operacional
Microservices separa Order, Inventory, Payment em serviços distintos, comunicando-se via rede (REST, gRPC, message queue).
@Service
public class OrderService {
private final RestClient restClient;
public OrderService(RestClient restClient) {
this.restClient = restClient;
}
public Order create(OrderRequest request) {
// Chama Inventory Service via HTTP
restClient.post()
.uri("http://inventory-service/reservations")
.body(new ReservationRequest(request.productId(), request.quantity()))
.retrieve()
.toBodilessEntity();
// Chama Payment Service via HTTP
restClient.post()
.uri("http://payment-service/charges")
.body(new ChargeRequest(request.customerId(), request.amount()))
.retrieve()
.toBodilessEntity();
return new Order(request);
}
}
Comparado com a versão monolith da seção 2, a diferença está em restClient.post(...): a chamada de método in-process se tornou uma chamada HTTP pela rede. As consequências:
- Cada chamada pode falhar por timeout de rede, serviço fora do ar, falha de DNS — erros que não existem em monolith.
- Não há mais transações ACID entre serviços. Você deve lidar com padrão saga ou consistência eventual.
- É necessário adicionar service discovery, load balancer e distributed tracing para debugar quando há erros.
3.1. Custos ocultos: transação distribuída
@Service
public class OrderSagaOrchestrator {
public void createOrder(OrderRequest request) {
try {
inventoryClient.reserve(request.productId(), request.quantity());
paymentClient.charge(request.customerId(), request.amount());
} catch (PaymentFailedException e) {
// Você deve escrever manualmente o passo de compensação para fazer rollback
inventoryClient.release(request.productId(), request.quantity());
throw e;
}
}
}
No monolith, o rollback é automático com @Transactional. Em microservices, você deve escrever manualmente a lógica compensate (release quando reserve falha). Este é o código que não existe na versão monolith, mas é obrigatório em microservices para evitar inconsistência de dados.
4. Critérios para decidir: não apenas seguindo o que empresas grandes fazem
4.1. Tamanho da equipe e Lei de Conway
Princípio de Conway: a estrutura do sistema reflete a estrutura da organização. Se você tem 5 desenvolvedores, dividi-los em 5 serviços significa que cada pessoa cuida de um serviço — ninguém revisa o código de ninguém, e você perde o benefício de ‘múltiplas pessoas analisando um bug’.
Referência prática:
1–15 dev, 1 equipe -> Monolith (modular)
15–50 dev, 2–5 equipes -> Monolith modular ou separar 2-3 serviços grandes
50+ dev, múltiplas equipes -> Microservices por bounded context
4.2. O release é realmente um gargalo?
Se sua equipe faz release do monolith a cada 2 semanas e não há conflitos de merge sérios, separar serviços para ‘release independente’ não traz valor — apenas adiciona custo operacional (CI/CD separado, monitoring separado, versionamento de API).
Microservices vale a pena quando: a equipe A precisa fazer deploy 10 vezes por dia para a feature Payment, enquanto a equipe B só faz deploy do Inventory 1 vez por semana. Separar permite que A não espere por B.
4.3. Capacidade de resiliência da infraestrutura
# docker-compose.yml - ilustra a complexidade mínima de infraestrutura para microservices
services:
order-service:
image: order-service:1.0
depends_on:
- inventory-service
- payment-service
- service-registry
inventory-service:
image: inventory-service:1.0
payment-service:
image: payment-service:1.0
service-registry:
image: eureka-server:1.0
api-gateway:
image: spring-cloud-gateway:1.0
Com apenas 3 serviços de negócio, você já precisa adicionar service-registry e api-gateway. Multiplicado por 20 serviços reais, a quantidade de partes móveis cresce exponencialmente — requer uma equipe DevOps forte para orquestração de containers, logging distribuído (ELK), tracing (Jaeger/Zipkin).
Nota: Se sua empresa ainda não tem alguém experiente em orquestração de containers e observability stack, microservices vai transformar cada incidente em uma sessão de ‘procurar uma agulha num palheiro’ através de múltiplos logs de serviços.
5. Monolith modular: opção intermediária frequentemente negligenciada
Monolith modular é um monolith mas o código é organizado em módulos independentes, comunicação por interfaces claras, sem dependências cruzadas de tabelas de banco de dados.
// Package: com.nonladev.shop.order
public interface InventoryPort {
void reserve(String productId, int quantity);
}
// Package: com.nonladev.shop.inventory
@Service
public class InventoryService implements InventoryPort {
@Override
public void reserve(String productId, int quantity) {
// lógica de negócio, acesso ao BD isolado do módulo de inventário
}
}
OrderService depende apenas da interface InventoryPort, desconhecendo os detalhes de implementação do módulo inventory. Dessa forma, se depois precisar separar inventory como um serviço independente, você apenas muda a implementação de InventoryPort para um cliente HTTP — o código que chama em OrderService não muda.
Esta é exatamente a estratégia de ‘começar com monolith, separar gradualmente quando necessário’ que muitos sistemas grandes (Shopify, GitHub nos estágios iniciais) aplicaram com sucesso.
6. Teste: medindo o custo de latência ao migrar de chamadas in-process para network calls
O exemplo de teste abaixo ilustra a diferença de latência — um fator que precisa ser medido na prática antes de decidir separar um serviço.
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
class OrderServiceLatencyTest {
@Test
void inProcessCallShouldBeFasterThanRemoteCall() {
InventoryPort inProcess = (productId, qty) -> { /* no-op */ };
long startInProcess = System.nanoTime();
inProcess.reserve("P-1", 1);
long inProcessDuration = System.nanoTime() - startInProcess;
InventoryPort remoteSimulated = (productId, qty) -> {
try {
Thread.sleep(20); // simula rede round-trip ~20ms
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
};
long startRemote = System.nanoTime();
remoteSimulated.reserve("P-1", 1);
long remoteDuration = System.nanoTime() - startRemote;
assertThat(remoteDuration).isGreaterThan(inProcessDuration);
}
}
Este teste não mede o desempenho real de sua infraestrutura, mas ilustra corretamente o ponto: cada vez que Order chama Inventory pela rede, você adiciona alguns até dezenas de milissegundos. Se uma solicitação precisar chamar uma cadeia de 5 serviços, a latência acumulada pode exceder o SLA — este é um custo frequentemente ignorado ao desenhar arquiteturas em papel.
7. Quando NÃO escolher microservices
- Equipe com menos de 10 pessoas: o custo operacional (CI/CD, monitoring, suporte) para múltiplos serviços supera o benefício da separação.
- Domain instável: se o negócio está constantemente mudando (startup em fase de busca de product-market fit), os limites do serviço estarão constantemente errados, separando e depois tendo que mesclar novamente — trabalho dobrado.
- Sem observabilidade: sem distributed tracing ou centralized logging, debugar erros entre serviços é praticamente impossível.
- Transações que precisam de strong consistency alta (por exemplo, core banking): manter em um transaction boundary único (monolith) é mais seguro do que implementar sagas complexas.
Por outro lado, microservices vale a pena investir quando: múltiplas equipes independentes, precisa escalar partes diferentes isoladamente (por exemplo, serviço de busca precisa de mais CPU que serviço de pedidos), ou quando precisa de diferentes tecnologias por domínio (Node.js para chat em tempo real, Java para billing).
8. Conclusão
Não existe arquitetura ‘perfeitamente correta’ — apenas uma arquitetura adequada ao tamanho da equipe, velocidade de release e capacidade operacional atual. Se não tem certeza, comece com monolith modular: mantenha interfaces claras entre os domínios, evite dependências cruzadas de banco de dados, para que quando realmente precisar separar (por scaling ou crescimento da equipe), você apenas mude a implementação, sem precisar reescrever todo o sistema. Microservices é uma ferramenta para resolver problemas de organização e escala, não um objetivo arquitetural em si.