Microservices é provavelmente o estilo arquitetural mais mal-aplicado da última década. Não porque seja uma ideia ruim — é uma ideia muito boa para os problemas certos. O problema é que ele foi adotado em escala massiva por razões erradas: hype, pressão de mercado, a crença de que "toda empresa de tecnologia séria usa microservices". O resultado foi uma geração de sistemas com toda a complexidade de sistemas distribuídos e nenhum dos benefícios que microservices prometem — porque os pré-requisitos para que microservices funcionem nunca foram satisfeitos.
O espectro de decomposição
Antes de comparar as opções, é útil entender o espectro. Em um extremo está o monolito como é frequentemente entendido de forma pejorativa: um único processo, sem separação de responsabilidade interna, onde qualquer parte do código pode chamar qualquer outra. Esse é o Big Ball of Mud. É um anti-padrão — mas muitas vezes o que as pessoas querem dizer quando dizem "monolito" é simplesmente "aplicação deployada como um único processo", o que não implica ausência de estrutura interna.
No outro extremo estão os microservices: múltiplos processos independentes, cada um com seu próprio banco de dados, seu próprio ciclo de deploy, sua própria linguagem potencialmente, se comunicando via rede (REST, gRPC, mensageria). Cada serviço é autônomo; a falha de um não deve derrubar os outros.
No meio está o monolito modular: um único processo, mas com módulos internos claramente delimitados — com interfaces públicas explícitas, sem acesso direto entre módulos passando por cima das interfaces, e possivelmente com banco de dados separado por módulo (ou schemas separados no mesmo banco). A disciplina estrutural de microservices sem o custo operacional de distribuição.
O anti-padrão: monolito distribuído
O pior resultado possível ao migrar de monolito para microservices é o monolito distribuído: múltiplos serviços que ainda dependem fortemente uns dos outros — que precisam ser deployados juntos, que têm acoplamento temporal (chamada síncrona em cadeia onde um depende da resposta do outro), e que compartilham banco de dados.
Você tem toda a complexidade de sistemas distribuídos (latência de rede, falhas parciais, tracing distribuído, service discovery, múltiplos deploys) sem nenhum dos benefícios (autonomia de deploy, isolamento de falha, escala independente). Martin Fowler nomeia esse anti-padrão explicitamente e o descreve como "the worst of all worlds".
O sinal mais claro de monolito distribuído: quando você precisa fazer um deploy de três serviços diferentes para entregar uma nova feature porque eles estão acoplados. Ou quando uma query de relatório precisa chamar cinco serviços em sequência e o timeout de qualquer um quebra o fluxo inteiro.
Se você tem microservices mas precisa deployar múltiplos ao mesmo tempo para entregar uma feature, ou se um serviço chama outro synchronously em cadeia com mais de dois saltos, você provavelmente tem um monolito distribuído.
O custo real de microservices
A conversa sobre microservices frequentemente subestima o custo. O benefício — autonomia de deploy, escala independente, isolamento de falha — é real mas condicional. O custo é real e incondicional.
Complexidade operacional. Cada serviço precisa de seu próprio pipeline de CI/CD, seu próprio processo de build e deploy, suas próprias configurações, seus próprios secrets. Monitorar dez serviços é mais difícil que monitorar um. Debugar um fluxo que passa por cinco serviços requer distributed tracing — e os traces precisam ser configurados, coletados, e correlacionados. Incidentes são mais difíceis de diagnosticar quando o erro está em qualquer um de dez serviços.
Latência de rede e falhas parciais. Uma chamada que antes era uma função call (microssegundos) passa a ser uma chamada de rede (milissegundos ou dezenas de milissegundos). Cada chamada pode falhar. Se o fluxo tem cinco chamadas síncronas, a disponibilidade composta é o produto das disponibilidades individuais: com cada serviço a 99.9%, cinco em cadeia resultam em ~99.5%.
Consistência eventual e transações distribuídas. Quando a lógica de negócio precisa de garantias transacionais entre dois serviços com bancos diferentes, você precisa de Saga ou 2-Phase Commit — ambos complexos. A consistência eventual que antes era implícita agora precisa ser projetada explicitamente.
Cognitive overhead. Para entender o comportamento de um fluxo, um engenheiro precisa entender múltiplos repositórios, múltiplos schemas de banco, múltiplos contratos de API. O onboarding de novos engenheiros é mais difícil. A curva para contribuir em um serviço que você nunca viu é maior.
Quando microservices justificam o custo
Microservices resolvem três problemas específicos com eficácia que outras abordagens não têm.
Autonomia de deploy por equipe. Quando duas equipes diferentes precisam deployar na mesma cadência e a coordenação de deploy está se tornando um gargalo, microservices permitem que cada equipe detenha o ciclo de vida completo do seu serviço. Isso é problema organizacional, não técnico — e microservices é a solução técnica para um problema organizacional específico. (Conway's Law em ação.)
Escala diferenciada. Quando módulos diferentes têm perfis de carga muito distintos — o módulo de streaming de vídeo precisa de GPU, o módulo de busca precisa de memória, o módulo de autenticação é CPU-bound — microservices permitem escalar cada um independentemente. Em monolito, você escala o processo inteiro (cara) ou aceita a ineficiência.
Isolamento de falha crítico. Quando a falha de um componente não deve afetar outro de forma alguma — como em sistemas de missão crítica onde o módulo de pagamento não pode ser derrubado por um bug no módulo de recomendação — microservices provêm isolamento em nível de processo. Em monolito, um OutOfMemoryError em qualquer parte mata o processo inteiro.
O modular monolith como primeira escolha
Para a maioria dos sistemas que não têm (ainda) os problemas que microservices resolvem, o monolito modular é a escolha correta. Ele oferece:
Fronteiras de módulo sem overhead de rede. Você pode ter separação de responsabilidade tão clara quanto microservices — módulos com interfaces públicas, sem acesso direto ao estado interno de outro módulo, com linguagem ubíqua por módulo. A diferença é que a chamada entre módulos é uma função call, não uma chamada de rede.
Transações ACID gratuitas. Dentro de um processo com um banco, você tem transações locais. Quando você precisar de consistência entre o módulo de Orders e o módulo de Inventory, é uma transaction — não um Saga com compensating transactions.
Caminho claro para microservices. Um monolito modular bem-delimitado pode ser decomposto em microservices quando o crescimento justificar. Os bounded contexts já estão identificados; as interfaces já estão definidas. A migração é extrair um módulo para um serviço — não um refactor massivo de um Big Ball of Mud.
Comparação de estrutura por abordagem
// Monolito modular — uma solution, múltiplos projetos com fronteiras claras
// ECommerce.sln
// ECommerce.Catalog/ — módulo com interface pública
// ICatalogModule.cs — interface pública do módulo
// Domain/ — private ao módulo
// Application/ — private ao módulo
// Infrastructure/ — private ao módulo
// ECommerce.Orders/
// IOrdersModule.cs
// Domain/
// Application/
// Infrastructure/
// ECommerce.Shared/ — tipos compartilhados mínimos
// CustomerId.cs
// ProductId.cs
// ECommerce.Api/ — host; compõe e expõe os módulos
// ICatalogModule — contrato público do módulo Catalog
public interface ICatalogModule
{
Task<ProductSnapshot?> GetProductAsync(ProductId id, CancellationToken ct);
Task<bool> IsInStockAsync(ProductId id, int qty, CancellationToken ct);
}
// Orders chama Catalog via interface — nunca diretamente nas classes internas
public class PlaceOrderHandler
{
private readonly ICatalogModule _catalog; // referência ao módulo, não à infra
public async Task Handle(PlaceOrderCommand cmd)
{
foreach (var line in cmd.Lines)
{
var inStock = await _catalog.IsInStockAsync(line.ProductId, line.Qty, ct);
if (!inStock) throw new OutOfStockException(line.ProductId);
}
// ... restante da lógica
}
}
// VERSUS microservices: chamada HTTP/gRPC, com latência e falha potencial
public class CatalogHttpClient : ICatalogModule
{
private readonly HttpClient _http;
public async Task<bool> IsInStockAsync(ProductId id, int qty, CancellationToken ct)
{
var response = await _http.GetAsync($"/catalog/{id}/stock?qty={qty}", ct);
response.EnsureSuccessStatusCode();
// potencial timeout, connection refused, 5xx — precisa de retry/breaker
return (await response.Content.ReadFromJsonAsync<StockResult>())!.IsAvailable;
}
}
ICatalogModule é a interface pública do módulo. PlaceOrderHandler nunca importa tipos internos de Catalog — fronteira respeitada via interface.
# Monolito modular — pacotes com fronteiras explícitas
# ecommerce/
# catalog/
# __init__.py — API pública do módulo (tudo que outros módulos podem usar)
# _domain.py — privado ao módulo (convenção: prefixo _)
# _repo.py
# orders/
# __init__.py
# _domain.py
# shared/
# value_objects.py
# catalog/__init__.py — apenas o que é público
from ._domain import Product, ProductId
from ._service import CatalogService
__all__ = ["Product", "ProductId", "CatalogService"]
# orders/_service.py — usa a API pública de catalog
from catalog import CatalogService, ProductId # OK — import público
# from catalog._domain import Product # ERRADO — quebra encapsulamento
class PlaceOrderService:
def __init__(self, catalog: CatalogService, orders: "OrderRepository"):
self._catalog = catalog
async def execute(self, cmd: PlaceOrderCommand) -> str:
for line in cmd.lines:
in_stock = await self._catalog.is_in_stock(line.product_id, line.qty)
if not in_stock:
raise OutOfStockError(line.product_id)
# módulos se comunicam via interface Python — chamada local, sem rede
# Módulo evoluindo para microservice: substitui CatalogService por HTTP client
import httpx
class CatalogHttpClient:
def __init__(self, base_url: str):
self._client = httpx.AsyncClient(base_url=base_url)
async def is_in_stock(self, product_id: str, qty: int) -> bool:
# Agora: latência de rede, retry necessário, falha potencial
r = await self._client.get(f"/catalog/{product_id}/stock", params={"qty": qty})
r.raise_for_status()
return r.json()["available"]
# PlaceOrderService não muda — ICatalogModule é satisfeita por qualquer implementação
from catalog._domain import Product é um erro de encapsulamento. A API pública fica em __init__.py — o prefixo _ é a fronteira por convenção.
// Monolito modular — packages com interfaces bem definidas
// internal/
// catalog/
// catalog.go — API pública: tipos e interfaces exportadas
// domain.go — unexported (minúscula): privado ao pacote
// repo.go — unexported
// orders/
// orders.go
// handler.go
// internal/catalog/catalog.go — API pública do módulo
package catalog
type Product struct {
ID string
Name string
Price float64
}
// Interface que outros módulos podem usar
type Module interface {
GetProduct(ctx context.Context, id string) (*Product, error)
IsInStock(ctx context.Context, id string, qty int) (bool, error)
}
// internal/orders/handler.go — usa a interface do módulo catalog
package orders
import "ecommerce/internal/catalog"
type PlaceOrderHandler struct {
catalog catalog.Module // interface, não implementação concreta
orders OrderRepository
}
func (h *PlaceOrderHandler) Handle(ctx context.Context, cmd PlaceOrderCommand) error {
for _, line := range cmd.Lines {
ok, err := h.catalog.IsInStock(ctx, line.ProductID, line.Qty)
if err != nil { return err }
if !ok { return ErrOutOfStock }
}
// chamada local — microssegundos, sem falha de rede
return h.orders.Save(ctx, NewOrder(cmd))
}
// Quando extrair para microservice: adapter HTTP que implementa catalog.Module
type catalogHTTPClient struct{ client *http.Client; baseURL string }
func (c *catalogHTTPClient) IsInStock(ctx context.Context, id string, qty int) (bool, error) {
// agora: latência, timeouts, retry policy, circuit breaker necessários
req, _ := http.NewRequestWithContext(ctx, "GET",
fmt.Sprintf("%s/catalog/%s/stock?qty=%d", c.baseURL, id, qty), nil)
resp, err := c.client.Do(req)
if err != nil { return false, err }
var result struct{ Available bool }
json.NewDecoder(resp.Body).Decode(&result)
return result.Available, nil
}
// PlaceOrderHandler não muda — recebe catalog.Module, não sabe se é local ou HTTP
Em Go, identifiers em minúscula são unexported — a fronteira é forçada pelo compilador. catalog.Module é a interface pública; o resto é privado ao package.
A migração de monolito modular para microservices
Quando um módulo do monolito precisa ser extraído, o processo é relativamente direto se as fronteiras foram mantidas: criar um novo serviço, implementar um adapter HTTP/gRPC/mensageria que satisfaz a interface do módulo, substituir a implementação local pelo adapter, e remover o módulo do monolito. O resto do sistema não sabe que a chamada deixou de ser local.
Isso só funciona se as fronteiras foram respeitadas desde o início. Um módulo que vaza seu estado interno para outros (outros módulos acessam tabelas diretamente, constroem queries que assumem o schema interno) não pode ser extraído sem um refactor massivo. A disciplina de fronteiras no monolito modular é o investimento que torna a extração futura barata.
Referências para aprofundar
- artigo Microservices — Martin Fowler & James Lewis. martinfowler.com, 2014.
- artigo MonolithFirst — Martin Fowler. martinfowler.com, 2015.
- artigo Distributed Monolith — Martin Fowler. martinfowler.com.
- livro Building Microservices — Sam Newman, 2ª ed. O'Reilly, 2021.
- livro Monolith to Microservices — Sam Newman. O'Reilly, 2019.
- livro Microservices Patterns — Chris Richardson. Manning, 2018.
- livro Domain-Driven Design — Eric Evans. Addison-Wesley, 2003.
- artigo Modular Monolith: the goldilocks architecture — Nick Tune. medium.com, 2020.
- artigo Modular Monolith vs Microservices — Vladimir Khorikov. enterprisecraftsmanship.com, 2019.
- paper How do committees invent? — Melvin Conway. Datamation, 1968.
- livro Release It! — Michael Nygard, 2ª ed. Pragmatic Bookshelf, 2018.
- livro Designing Data-Intensive Applications — Martin Kleppmann. O'Reilly, 2017.