MÓDULO 15 · CONCEITO 05 DE 12

Microservices vs Modular Monolith vs Monolith

O monolito distribuído como anti-padrão, o custo real de microservices, e por que modular monolith é muitas vezes a resposta honesta

Tempo de leitura ~22 min Pré-requisito 04 · Clean Architecture Próximo 06 · Event-Driven Architecture

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.

sinal de alerta

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

C# — Solution multi-projeto com interfaces públicas por módulo
// 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.

Python — __init__.py como contrato público, prefixo _ para internos
# 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.

Go — identifiers unexported como fronteira de módulo
// 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

  1. artigo Microservices — Martin Fowler & James Lewis. martinfowler.com, 2014. O artigo seminal — pré-requisitos para microservices funcionarem, não apenas a definição.
  2. artigo MonolithFirst — Martin Fowler. martinfowler.com, 2015. O argumento para começar com monolito modular e migrar apenas quando os problemas aparecerem.
  3. artigo Distributed Monolith — Martin Fowler. martinfowler.com. O anti-padrão nomeado — toda a complexidade de sistemas distribuídos sem nenhum dos benefícios.
  4. livro Building Microservices — Sam Newman, 2ª ed. O'Reilly, 2021. O guia mais completo sobre microservices — decomposição, comunicação, dados, deploy e operação.
  5. livro Monolith to Microservices — Sam Newman. O'Reilly, 2019. Técnicas práticas de migração: Strangler Fig, ACL, decomposição incremental de banco.
  6. livro Microservices Patterns — Chris Richardson. Manning, 2018. Padrões de decomposição, Saga, CQRS e integração — a visão mais completa de patterns de microservices.
  7. livro Domain-Driven Design — Eric Evans. Addison-Wesley, 2003. Bounded Context como guia para fronteiras de módulo e serviço — a base intelectual para decomposição.
  8. artigo Modular Monolith: the goldilocks architecture — Nick Tune. medium.com, 2020. Argumento detalhado para modular monolith como escolha padrão antes de microservices.
  9. artigo Modular Monolith vs Microservices — Vladimir Khorikov. enterprisecraftsmanship.com, 2019. Comparação detalhada com análise de trade-offs e quando cada um é a escolha correta.
  10. paper How do committees invent? — Melvin Conway. Datamation, 1968. A Lei de Conway — sistemas refletem a estrutura de comunicação das organizações que os criam.
  11. livro Release It! — Michael Nygard, 2ª ed. Pragmatic Bookshelf, 2018. O custo operacional real de sistemas distribuídos — timeouts, bulkheads, circuit breakers.
  12. livro Designing Data-Intensive Applications — Martin Kleppmann. O'Reilly, 2017. Caps 8-9: os problemas fundamentais e inevitáveis de sistemas distribuídos.