Arquitetura é a arte de gerenciar trade-offs
Todo mundo aprende os padrões: microsserviços, CQRS, Event Sourcing, Clean Architecture. Os tutoriais mostram os benefícios. Poucos mostram o preço.
Na prática, arquitetura é uma sequência de decisões com consequências. Cada escolha abre possibilidades e fecha outras. O problema não é escolher o "padrão certo" — é entender o que você está trocando em cada decisão e se esse trade-off faz sentido para o seu contexto.
Este artigo não tem "a resposta certa". Tem as perguntas certas.
Monólito vs Microsserviços: a decisão mais mal-entendida
Microsserviços virou o default. Qualquer novo projeto "tem que ser" microsserviços. O resultado: times de 3 pessoas gerenciando 12 serviços com Kubernetes, descobrindo que 60% do tempo vai para infraestrutura e não para o produto.
O que os tutoriais mostram sobre microsserviços
Escalabilidade independente por serviço. Times autônomos. Deploy sem coordenação. Fault isolation. Tecnologias diferentes por domínio.
O que os tutoriais não mostram
Transactions distribuídas são muito mais complexas que transactions locais. Debugging cross-service sem tracing distribuído é um pesadelo. Consistência eventual cria classes de bugs que você nunca viu antes. Latência de rede entre serviços se acumula. Você precisa de infraestrutura de observabilidade completa só para funcionar (service mesh, distributed tracing, correlation IDs, centralized logging).
Quando microsserviços fazem sentido
✅ Times de 20+ pessoas que precisam de autonomia de deploy
✅ Partes do sistema com necessidades de escala radicalmente diferentes
(ex: serviço de busca recebe 100x mais tráfego que checkout)
✅ Domínios com bounded contexts muito bem definidos
✅ Você já tem o sistema funcionando como monólito e sabe onde escalar
✅ Infraestrutura como código é madura na sua equipe
Quando monólito modular é melhor
✅ Times pequenos (até 10-15 devs)
✅ Domínio ainda sendo descoberto (startup, novo produto)
✅ Sem budget de infraestrutura para Kubernetes + observabilidade
✅ Consistência transacional é crítica para o negócio
✅ Você não sabe ainda quais partes vão escalar
Um monólito modular bem feito — com namespaces isolados, dependências internas controladas via interfaces, e fronteiras respeitadas — pode ser evoluído para microsserviços depois. O contrário é muito mais caro.
// Monólito modular: módulos com fronteiras claras, mesma solução
// Vendas.Application não acessa diretamente Estoque.Infrastructure
// Comunicação via interfaces ou eventos internos
public class ConfirmarPedidoHandler : IRequestHandler<ConfirmarPedidoCommand>
{
private readonly IPedidoRepository _pedidoRepo;
private readonly IEstoqueService _estoque; // interface — pode ser local ou remoto
// Hoje: IEstoqueService é implementado localmente no mesmo processo
// Amanhã: IEstoqueService pode ser um client HTTP sem mudar o handler
}
Consistência eventual vs Consistência forte
Em sistemas distribuídos, o teorema CAP (Consistency, Availability, Partition tolerance) diz que você só pode ter dois dos três. Na prática, partições de rede acontecem — então a escolha real é entre consistência e disponibilidade.
Consistência forte (transações ACID)
// Transação única: pedido + reserva de estoque + pagamento
await using var tx = await _db.Database.BeginTransactionAsync();
try
{
await _pedidoRepo.AddAsync(pedido);
await _estoqueRepo.ReservarAsync(pedido.Itens);
await _pagamentoRepo.RegistrarCobrancaAsync(pedido.Total);
await _db.SaveChangesAsync();
await tx.CommitAsync();
}
catch
{
await tx.RollbackAsync();
throw;
}
Prós: lógica simples, sem edge cases de inconsistência, fácil de raciocinar.
Contras: funciona só no mesmo banco. Não escala horizontalmente. Locks podem criar gargalos.
Consistência eventual (Saga Pattern)
// Saga de confirmação de pedido: sequência de eventos
// Passo 1: PedidoCriado → reservar estoque
// Passo 2: EstoqueReservado → processar pagamento
// Passo 3: PagamentoConfirmado → confirmar pedido
// Compensações: se pagamento falha → liberar estoque, cancelar pedido
public class ConfirmarPedidoSaga :
MassTransitStateMachine<ConfirmarPedidoSagaState>
{
public ConfirmarPedidoSaga()
{
During(Initial,
When(PedidoCriado)
.Then(ctx => ctx.Saga.PedidoId = ctx.Message.PedidoId)
.Publish(ctx => new ReservarEstoqueCommand(ctx.Saga.PedidoId))
.TransitionTo(AguardandoEstoque));
During(AguardandoEstoque,
When(EstoqueReservado)
.Publish(ctx => new ProcessarPagamentoCommand(ctx.Saga.PedidoId))
.TransitionTo(AguardandoPagamento),
When(EstoqueIndisponivel)
.Publish(ctx => new CancelarPedidoCommand(ctx.Saga.PedidoId))
.Finalize());
// ... compensações e timeout handlers
}
}
Prós: funciona entre serviços e bancos diferentes. Alta disponibilidade. Escala horizontalmente.
Contras: complexidade exponencial. Você precisa lidar com timeouts, mensagens duplicadas, ordem de chegada, compensações. Debugging é difícil sem tracing.
Regra prática: use consistência forte onde puder. Use consistência eventual apenas onde a escala ou os limites de serviço exigem. A maioria das empresas brasileiras não tem volume para precisar de consistência eventual no core do negócio.
CQRS: quando vale o custo
CQRS separa modelos de leitura e escrita. O benefício real é poder otimizar cada lado independentemente. O custo é complexidade.
CQRS leve (mesmo banco, modelos separados)
// Comando: usa o modelo de domínio rico
public class ConfirmarPedidoHandler : IRequestHandler<ConfirmarPedidoCommand>
{
// Usa repositório → EF Core → domain model completo
}
// Query: vai direto ao banco, sem passar pelo domínio
public class GetPedidosClienteHandler : IRequestHandler<GetPedidosClienteQuery, List<PedidoSummaryDto>>
{
private readonly AppDbContext _db;
public async Task<List<PedidoSummaryDto>> Handle(...)
{
return await _db.Pedidos
.Where(p => p.ClienteId == query.ClienteId)
.Select(p => new PedidoSummaryDto(p.Id, p.Status, p.ValorTotal, p.CreatedAt))
.OrderByDescending(p => p.CreatedAt)
.Take(20)
.ToListAsync(ct);
}
}
Custo: separação de código (Commands vs Queries). Benefício: queries rápidas sem carregar o domínio.
CQRS completo (bancos separados + projeções)
Modelo de escrita em PostgreSQL relacional. Modelo de leitura em Redis/Elasticsearch/banco desnormalizado. Projeções atualizam o modelo de leitura via Domain Events.
Custo: infraestrutura dupla, consistência eventual entre escrita e leitura, complexidade de projeções. Benefício: leituras extremamente rápidas, escala independente.
Recomendação: comece com CQRS leve. Migre para CQRS completo apenas se as leituras forem um gargalo demonstrado — não antecipado.
Event Sourcing: poder e responsabilidade
Event Sourcing armazena cada mudança de estado como um evento imutável. O estado atual é reconstruído pela soma dos eventos.
// Event store: só insere, nunca atualiza
| AggregateId | Version | EventType | EventData | OccurredAt |
|-------------|---------|------------------|--------------------|------------|
| pedido-123 | 1 | PedidoCriado | {clienteId, ...} | 2026-03-01 |
| pedido-123 | 2 | ItemAdicionado | {produtoId, qty} | 2026-03-01 |
| pedido-123 | 3 | ItemAdicionado | {produtoId, qty} | 2026-03-01 |
| pedido-123 | 4 | PedidoConfirmado | {valorTotal} | 2026-03-02 |
O que você ganha: histórico completo e auditável de tudo que aconteceu. Time travel (recriar o estado em qualquer ponto do tempo). Replay de eventos para criar novas projeções.
O que você paga: queries complexas exigem projeções separadas (CQRS é quase obrigatório). Snapshots para não reconstruir estado desde o início. Schema evolution é complexo (eventos antigos precisam continuar sendo lidos por código novo). A maioria da equipe nunca trabalhou com isso.
Onde Event Sourcing brilha: sistemas financeiros (cada transação precisa ser auditável), sistemas de reserva (rastrear cada mudança de disponibilidade), sistemas regulados (LGPD, BACEN exigem histórico completo).
Onde Event Sourcing é over-engineering: qualquer CRUD, qualquer sistema onde o estado atual é suficiente, qualquer equipe sem experiência sólida com o padrão.
Comunicação síncrona vs assíncrona
HTTP/gRPC síncrono
// Chama serviço externo e aguarda resposta
var resultado = await _pagamentoClient.ProcessarAsync(pedidoId, valor, ct);
if (!resultado.Aprovado)
throw new PagamentoRecusadoException(resultado.Motivo);
Prós: simples de implementar, debugar e raciocinar. Resposta imediata. Bom para operações que precisam de resultado para continuar.
Contras: se o serviço chamado cair, você cai junto. Latência se acumula em chains. Não escala bem com picos.
Mensageria assíncrona
// Publica evento e continua — não aguarda processamento
await _messageBus.PublishAsync(new PedidoConfirmadoEvent(pedidoId));
return Results.Accepted();
// Em outro serviço, em outro momento:
public class ProcessarPedidoConsumer : IConsumer<PedidoConfirmadoEvent>
{
public async Task Consume(ConsumeContext<PedidoConfirmadoEvent> ctx)
{
await _processador.ProcessarAsync(ctx.Message.PedidoId);
}
}
Prós: desacopla produtor de consumidor. Absorve picos. Retry automático. Fault isolation.
Contras: consistência eventual. Debugging difícil. Mensagens out-of-order. Idempotência obrigatória. Infraestrutura de broker (RabbitMQ, Kafka, SQS).
Regra prática: use síncrono quando precisa da resposta para continuar ou quando falha do serviço downstream deve falhar a operação. Use assíncrono para side effects (notificações, atualizações de outros contextos, processamento em background).
Banco de dados: o trade-off mais subestimado
SQL relacional (PostgreSQL, SQL Server)
Melhor escolha para: dados relacionais, transactions ACID, queries complexas com joins, schema bem definido, integridade referencial.
Limitações: escala horizontal é cara (sharding é complexo), schema migration em produção pode causar downtime, escrita pode criar gargalo em alta concorrência.
MongoDB / DocumentDB
Melhor escolha para: documentos com estrutura variável, escritas de alta velocidade, dados desnormalizados que são sempre acessados juntos.
Limitações: sem transactions multi-documento confiáveis no free tier, sem joins nativos, inconsistências são mais fáceis de introduzir, schema validation precisa ser aplicado pela aplicação.
Redis
Melhor escolha para: cache, sessões, filas, pub/sub, leaderboards, rate limiting.
Limitações: memória é cara, persistência não é o foco primário, não substitui banco principal.
Decisão pragmática para 90% dos projetos: PostgreSQL como banco principal + Redis para cache. O PostgreSQL moderno aguenta muito mais do que a maioria dos projetos vai precisar — com JSONb, full text search, e suporte a dados semi-estruturados.
A decisão mais importante: reversibilidade
Jeff Bezos divide decisões em dois tipos: Type 1 (irreversíveis — precisam de muito cuidado) e Type 2 (reversíveis — podem ser feitas rápido e corrigidas depois).
Escolha do banco de dados → Type 1. Migrar de SQL para NoSQL depois de ter dados é caro.
Estrutura de tabelas → Type 2 com custo. Migrations são possíveis.
Escolha de ORM → Type 2. Pode trocar sem reescrever domínio (com Clean Architecture).
Monólito vs microsserviços → Type 1 na prática. Decompor um monólito ruim é caro.
Framework de testes → Type 2. Fácil de trocar.
Para decisões Type 1, invista mais tempo na análise. Para Type 2, decida rápido e siga em frente.
ADR: Architecture Decision Records
A melhor prática para documentar decisões arquiteturais é o ADR (Architecture Decision Record) — um documento curto que registra o contexto, a decisão tomada e as consequências:
# ADR-001: Escolha do banco de dados principal
**Status:** Aceito
**Data:** 2026-03-16
## Contexto
Sistema de pedidos com ~50.000 transações/dia. Dados altamente relacionais (pedido → itens → cliente → endereço). Equipe com forte experiência em SQL.
## Decisão
PostgreSQL 16 como banco principal.
## Alternativas consideradas
- MongoDB: descartado pela natureza relacional dos dados e necessidade de transactions ACID
- MySQL: preterido pelos recursos avançados do PostgreSQL (JSONB, full text, arrays)
- SQL Server: custo de licença não justificado
## Consequências
- Precisamos de migrations com EF Core (custo: baixo)
- Escala horizontal via read replicas quando necessário
- Time já conhece PostgreSQL — curva de aprendizado zero
ADRs ficam no repositório junto com o código. São pequenos, objetivos, e respondem a pergunta "por que essa decisão foi tomada?" — que você vai fazer 6 meses depois quando precisar revisitar.
Conclusão
Não existe arquitetura perfeita — existe arquitetura adequada para um contexto. O arquiteto que entende trade-offs é mais valioso que o que conhece mais padrões.
As decisões que mais importam: estrutura modular desde o início (mais fácil separar depois que unir), consistência forte onde possível (consistência eventual tem custo alto), banco SQL como default (NoSQL quando há razão específica), monólito modular antes de microsserviços (evoluir é mais barato que decompor).
E acima de tudo: documente as decisões com ADRs. O desenvolvedor que vai manter o sistema daqui a 2 anos pode ser você mesmo — e você vai agradecer por ter registrado o porquê.