Arquitetura de Software
O Papel do Arquiteto de Software
ARQUITETURA NÃO É:
- Desenhar diagramas bonitos e nunca implementar
- Escolher a tecnologia mais hype do momento
- Tomar decisões sozinho em uma torre de marfim
ARQUITETURA É:
- Tomar decisões estruturais que são CARAS de mudar depois
- Analisar trade-offs e comunicá-los claramente para stakeholders
- Definir restrições que guiam decisões de design
- Criar FITNESS FUNCTIONS que validam automaticamente as decisões
"The architecture of a software system is the set of structures
needed to reason about the system." — SEI/CMU
LEIS FUNDAMENTAIS:
1. Lei de Conway: "A estrutura do software reflete a estrutura organizacional"
Times separados → serviços separados → APIs entre eles
Solução: alinhar times com domínios de negócio (Inverse Conway Maneuver)
2. Lei de Gall: "Um sistema complexo que funciona invariavelmente
evoluiu de um sistema simples que funcionava."
Implicação: comece simples, evolua incrementalmente
3. Última Decisão Responsável: adie decisões arquiteturais até o
último momento responsável — quando tiver mais informação
Características Arquiteturais (os “-ilities”)
CARACTERÍSTICAS OPERACIONAIS:
┌─────────────────┬────────────────────────────────────────────────┐
│ Disponibilidade │ % de tempo que o sistema está acessível │
│ │ 99.9% = 8.76h/ano de downtime │
│ │ 99.99% = 52.6min/ano │
├─────────────────┼────────────────────────────────────────────────┤
│ Escalabilidade │ Capacidade de lidar com aumento de carga │
│ │ Vertical: máquina maior │
│ │ Horizontal: mais máquinas │
├─────────────────┼────────────────────────────────────────────────┤
│ Performance │ Latência e throughput │
│ │ p99 < 200ms? 10k requests/s? │
├─────────────────┼────────────────────────────────────────────────┤
│ Confiabilidade │ Probabilidade de funcionar corretamente │
│ │ Tolerância a falhas, graceful degradation │
├─────────────────┼────────────────────────────────────────────────┤
│ Resiliência │ Capacidade de se recuperar de falhas │
│ │ Circuit breakers, retries, fallbacks │
└─────────────────┴────────────────────────────────────────────────┘
CARACTERÍSTICAS ESTRUTURAIS:
┌─────────────────┬────────────────────────────────────────────────┐
│ Manutenibilidade│ Facilidade de modificar e corrigir │
│ │ Modularidade, baixo acoplamento │
├─────────────────┼────────────────────────────────────────────────┤
│ Testabilidade │ Facilidade de testar componentes │
│ │ Injeção de dependência, contratos claros │
├─────────────────┼────────────────────────────────────────────────┤
│ Deployability │ Facilidade e frequência de deploy │
│ │ CI/CD, feature flags, blue-green │
├─────────────────┼────────────────────────────────────────────────┤
│ Extensibilidade │ Facilidade de adicionar funcionalidades │
│ │ Plugins, hooks, event-driven │
└─────────────────┴────────────────────────────────────────────────┘
TRADE-OFF FUNDAMENTAL:
Nem todas as características podem ser maximizadas simultaneamente.
Escolher 3-5 características prioritárias para o contexto do sistema.
Exemplo: Sistema de pagamentos
Prioridades: Confiabilidade > Segurança > Performance > Disponibilidade
Aceita: menor Deployability, menor Extensibilidade
Exemplo: Rede social
Prioridades: Escalabilidade > Disponibilidade > Performance
Aceita: consistência eventual (CAP theorem)
Estilos Arquiteturais
1. LAYERED ARCHITECTURE (monolítica em camadas):
┌────────────────────────────────────┐
│ Presentation Layer │ Controllers, Views
├────────────────────────────────────┤
│ Business Layer │ Services, Domain Logic
├────────────────────────────────────┤
│ Persistence Layer │ Repositories, DAOs
├────────────────────────────────────┤
│ Database Layer │ Database Engine
└────────────────────────────────────┘
Regra: cada camada só acessa a camada imediatamente abaixo.
Vantagens: simplicidade, separação de concerns, familiar.
Desvantagens: acoplamento vertical, difícil escalar parcialmente.
Quando usar: aplicações CRUD, times pequenos, MVPs.
2. MODULAR MONOLITH (evolução da layered):
┌─────────────────────────────────────────────┐
│ API Gateway │
├──────────┬──────────┬──────────┬────────────┤
│ Auth │ Orders │ Users │ Products │ Módulos independentes
│ Module │ Module │ Module │ Module │
│ ┌────┐ │ ┌────┐ │ ┌────┐ │ ┌────┐ │
│ │Svc │ │ │Svc │ │ │Svc │ │ │Svc │ │ Cada módulo tem
│ │Repo│ │ │Repo│ │ │Repo│ │ │Repo│ │ suas próprias camadas
│ │DB │ │ │DB │ │ │DB │ │ │DB │ │
│ └────┘ │ └────┘ │ └────┘ │ └────┘ │
└──────────┴──────────┴──────────┴────────────┘
Shared Database (ou schemas separados)
Comunicação entre módulos: interfaces públicas (não acesso direto ao DB)
Vantagens: escalabilidade organizacional sem complexidade distribuída.
Precursor natural para microsserviços (extrair módulo = extrair serviço).
3. MICROKERNEL (plugin architecture):
┌────────────────────────────────────────┐
│ Core System │
│ (funcionalidade mínima e estável) │
├────────┬────────┬────────┬─────────────┤
│Plugin A│Plugin B│Plugin C│ Plugin D │
│Tax BR │Tax US │Export │ Custom │
└────────┴────────┴────────┴─────────────┘
Quando usar: sistemas com regras que variam por cliente/região.
Exemplos: IDEs, browsers, ERP com módulos por país.
4. EVENT-DRIVEN ARCHITECTURE:
┌──────────┐ ┌──────────────────┐ ┌──────────────┐
│ Producer │ → │ Event Broker │ → │ Consumer A │
│ (Orders) │ │ (Kafka/RabbitMQ) │ │ (Email) │
└──────────┘ │ │ → ├──────────────┤
│ │ │ Consumer B │
└──────────────────┘ │ (Inventory) │
└──────────────┘
Dois padrões:
- Event Notification: "algo aconteceu" (event contém mínimo de dados)
Consumers buscam dados adicionais quando necessário.
- Event-Carried State Transfer: event contém todos os dados necessários
Consumers são autossuficientes (eliminam chamadas síncronas).
Topologias:
- Broker: eventos publicados em tópicos, consumers se inscrevem
Mais desacoplado, mais difícil garantir ordem de processamento.
- Mediator: orquestrador central coordena workflow
Mais controle, ponto único de falha.
// Event-Driven na prática:
// ACOPLAMENTO FORTE (chamada síncrona):
async function createOrder(data) {
const order = await saveOrder(data);
await emailService.sendConfirmation(order); // Se falhar, tudo falha
await inventoryService.reduceStock(order); // Acoplado!
await analyticsService.trackPurchase(order); // 3 dependências síncronas
return order;
// Latência total = soma de todas as chamadas
// Disponibilidade = produto das disponibilidades (99.9%^3 = 99.7%)
}
// ACOPLAMENTO FRACO (event-driven):
async function createOrder(data) {
const order = await saveOrder(data);
// Transactional Outbox Pattern: salvar evento na mesma transação
await db.transaction(async (tx) => {
await tx.insert('orders', order);
await tx.insert('outbox_events', {
aggregate_type: 'Order',
aggregate_id: order.id,
event_type: 'OrderCreated',
payload: JSON.stringify(order),
});
});
// Processo separado (Debezium, polling) publica eventos do outbox
return order;
// Latência = apenas saveOrder
// Email, estoque, analytics processam assincronamente
// Se falharem: retry com backoff, dead letter queue
}
// Consumers independentes:
eventBus.subscribe('OrderCreated', async (event) => {
await sendConfirmationEmail(event.payload);
});
eventBus.subscribe('OrderCreated', async (event) => {
await reduceInventory(event.payload);
});
// Adicionar novo consumer NÃO requer mudança no producer
// Cada consumer tem retry e dead letter queue independente
Hexagonal Architecture (Ports and Adapters)
A arquitetura hexagonal, proposta por Alistair Cockburn em 2005, inverte a dependência clássica: o domínio fica no centro e não depende de nada externo. Toda comunicação com o mundo exterior passa por ports (interfaces) e adapters (implementações).
┌─────────────────────────────┐
│ DRIVING ADAPTERS │
│ (quem INICIA a interação) │
│ │
│ REST Controller │
│ GraphQL Resolver │
│ CLI Command │
│ Event Consumer │
└──────────┬──────────────────┘
│
┌──────────▼──────────────────┐
│ DRIVING PORTS │
│ (interfaces de entrada) │
│ │
│ CreateOrderUseCase │
│ CancelOrderUseCase │
└──────────┬──────────────────┘
│
┌──────────▼──────────────────┐
│ DOMAIN CORE │
│ (regras de negócio puras) │
│ │
│ Order, Product, Payment │
│ Domain Services │
│ Domain Events │
│ Value Objects │
└──────────┬──────────────────┘
│
┌──────────▼──────────────────┐
│ DRIVEN PORTS │
│ (interfaces de saída) │
│ │
│ OrderRepository (interface) │
│ PaymentGateway (interface) │
│ NotificationService (iface) │
└──────────┬──────────────────┘
│
┌──────────▼──────────────────┐
│ DRIVEN ADAPTERS │
│ (quem RECEBE a interação) │
│ │
│ PostgresOrderRepository │
│ StripePaymentGateway │
│ SendGridNotificationService │
└─────────────────────────────┘
REGRA DE DEPENDÊNCIA:
→ Adapters dependem de Ports
→ Ports dependem do Domain
→ Domain NÃO depende de NADA externo
// === DOMAIN CORE (zero dependências externas) ===
// Value Object
class Money {
constructor(
readonly amount: number,
readonly currency: string
) {
if (amount < 0) throw new Error('Amount cannot be negative');
}
add(other: Money): Money {
if (this.currency !== other.currency) throw new Error('Currency mismatch');
return new Money(this.amount + other.amount, this.currency);
}
}
// Entity
class Order {
private constructor(
readonly id: string,
readonly items: OrderItem[],
private _status: OrderStatus
) {}
static create(items: OrderItem[]): Order {
if (items.length === 0) throw new Error('Order must have items');
return new Order(crypto.randomUUID(), items, 'pending');
}
get total(): Money {
return this.items.reduce(
(sum, item) => sum.add(item.subtotal),
new Money(0, 'BRL')
);
}
confirm(): void {
if (this._status !== 'pending') throw new Error('Only pending orders can be confirmed');
this._status = 'confirmed';
}
}
// === DRIVEN PORT (interface — contrato de saída) ===
interface OrderRepository {
save(order: Order): Promise<void>;
findById(id: string): Promise<Order | null>;
}
interface PaymentGateway {
charge(amount: Money, method: PaymentMethod): Promise<PaymentResult>;
}
// === DRIVING PORT (use case — contrato de entrada) ===
class CreateOrderUseCase {
constructor(
private readonly orders: OrderRepository,
private readonly payments: PaymentGateway
) {}
async execute(input: CreateOrderInput): Promise<Order> {
const order = Order.create(input.items);
const payment = await this.payments.charge(order.total, input.paymentMethod);
if (payment.status === 'approved') {
order.confirm();
}
await this.orders.save(order);
return order;
}
}
// === DRIVEN ADAPTER (implementação concreta) ===
class PostgresOrderRepository implements OrderRepository {
constructor(private readonly pool: Pool) {}
async save(order: Order): Promise<void> {
await this.pool.query(
'INSERT INTO orders (id, items, status) VALUES ($1, $2, $3)',
[order.id, JSON.stringify(order.items), order.status]
);
}
async findById(id: string): Promise<Order | null> {
const result = await this.pool.query('SELECT * FROM orders WHERE id = $1', [id]);
return result.rows[0] ? this.mapToDomain(result.rows[0]) : null;
}
}
// === DRIVING ADAPTER (controller HTTP) ===
class OrderController {
constructor(private readonly createOrder: CreateOrderUseCase) {}
async handlePost(req: Request, res: Response): Promise<void> {
const order = await this.createOrder.execute(req.body);
res.status(201).json({ orderId: order.id });
}
}
QUANDO USAR HEXAGONAL:
✅ Domínio complexo com muitas regras de negócio
✅ Múltiplos adapters (REST + GraphQL + CLI, Postgres + MongoDB)
✅ Testes de domínio sem infraestrutura (puro unit test)
✅ Times grandes onde isolamento de camadas reduz conflitos
QUANDO NÃO USAR:
❌ CRUDs simples (overhead de abstrações sem benefício)
❌ Scripts e lambdas (vida curta, poucas dependências)
❌ Protótipos (velocidade > arquitetura)
Microsserviços e Service Mesh
MICROSSERVIÇOS — quando faz sentido:
PRÉ-REQUISITOS (sem eles, microsserviços vão falhar):
□ CI/CD maduro (deploy automatizado e confiável)
□ Observabilidade robusta (métricas, logs, traces distribuídos)
□ Equipe experiente com sistemas distribuídos
□ Domínio de negócio bem entendido (bounded contexts claros)
□ Necessidade real de escalar times independentemente
□ Container orchestration (Kubernetes ou similar)
COMPLEXIDADES INTRODUZIDAS:
- Rede: latência, falhas parciais, retry storms
- Consistência: transações distribuídas (Saga pattern)
- Debugging: trace distribuído, correlação de logs
- Deploy: orquestração de deploys interdependentes
- Teste: testes de contrato, testes de integração entre serviços
- Operacional: mais serviços = mais coisas para monitorar/manter
PADRÕES DE RESILIÊNCIA:
// Circuit Breaker — evitar cascade failure:
// Estado: CLOSED → OPEN → HALF-OPEN → CLOSED
const CircuitBreaker = require('opossum');
const breaker = new CircuitBreaker(callPaymentService, {
timeout: 3000, // Timeout por chamada: 3s
errorThresholdPercentage: 50, // Abre se > 50% de falhas
resetTimeout: 30000, // Tenta fechar após 30s
volumeThreshold: 10, // Mínimo de chamadas antes de avaliar
});
breaker.on('open', () => {
logger.warn('Circuit breaker ABERTO para payment service');
metrics.inc('circuit_breaker_open_total');
});
breaker.on('halfOpen', () => {
logger.info('Circuit breaker HALF-OPEN — testando payment service');
});
breaker.fallback(() => {
// Fallback quando circuito está aberto:
return { status: 'pending', message: 'Pagamento será processado em breve' };
});
// Uso:
const result = await breaker.fire(orderData);
// Retry com exponential backoff:
async function retryWithBackoff(fn, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxRetries) throw error;
const delay = Math.min(1000 * Math.pow(2, attempt) + Math.random() * 1000, 30000);
// Jitter aleatório evita thundering herd
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Bulkhead — isolar falhas por dependência:
// Pool de conexões separado para cada serviço externo
// Se payment service travar, não consome conexões do email service
const paymentPool = new ConnectionPool({ max: 10 });
const emailPool = new ConnectionPool({ max: 5 });
SERVICE MESH (Istio, Linkerd):
Sidecar proxy intercepta todo tráfego de rede do serviço.
Sem mudar o código da aplicação, adiciona:
┌─────────────────────────────────┐
│ Pod │
│ ┌──────────┐ ┌─────────────┐ │
│ │ App │ │ Envoy │ │ ← Sidecar proxy
│ │ Container│←→│ Proxy │←──→ Rede
│ └──────────┘ └─────────────┘ │
└─────────────────────────────────┘
FUNCIONALIDADES DO SERVICE MESH:
- mTLS automático entre serviços (zero-trust networking)
- Retry, timeout, circuit breaker (configuráveis via YAML)
- Canary deployment com traffic splitting (90/10)
- Rate limiting por serviço
- Observabilidade (métricas, traces) sem instrumentação
- Fault injection para chaos engineering
QUANDO USAR:
- Muitos microsserviços (>10) com comunicação intensa
- Requisitos fortes de segurança (mTLS obrigatório)
- Necessidade de traffic management avançado
- NÃO usar para poucos serviços — overhead de operação alto
Architecture Decision Records (ADRs)
# ADR 001: Usar PostgreSQL como banco de dados primário
## Status
Aceito
## Contexto
Precisamos escolher um banco de dados relacional para a aplicação principal.
O time tem experiência com PostgreSQL e MySQL. A aplicação tem requisitos de:
- JSONB para dados semi-estruturados
- Full-text search básico
- Transações complexas com múltiplas tabelas
- Estimativa de crescimento: 50M linhas em 2 anos
## Decisão
Usar PostgreSQL 16 como banco de dados primário, hospedado no Amazon RDS.
## Alternativas Consideradas
### MySQL 8
- Prós: familiar para parte do time, boa performance para reads simples
- Contras: JSONB inferior, sem CTEs recursivas otimizadas, window functions
menos maduras, partitioning mais limitado
### DynamoDB
- Prós: serverless, auto-scaling, baixa latência consistente
- Contras: modelo relacional necessário, queries ad-hoc difíceis,
custo imprevisível para workloads de scan
## Consequências
- Time precisa se atualizar em features específicas do PostgreSQL 16
- Adotar PgBouncer para connection pooling
- Monitorar com pg_stat_statements desde o início
- Backups configurados com pgBackRest + WAL archiving para S3
- Custo estimado de RDS: ~$150/mês (db.r6g.large, Multi-AZ)
## Data
2024-01-15
ESTRUTURA DE ADRs NO REPOSITÓRIO:
docs/
├── adr/
│ ├── 0001-usar-postgresql.md
│ ├── 0002-adotar-event-driven.md
│ ├── 0003-migrar-para-typescript.md
│ ├── 0004-estrategia-de-cache.md
│ └── template.md
DICAS:
- Numerar sequencialmente (imutável)
- Status: Proposto → Aceito → Deprecado → Substituído por ADR-XXX
- Incluir alternativas consideradas (mostra que houve análise)
- Incluir consequências (positivas E negativas)
- Curto e direto (1-2 páginas)
- Revisado pelo time (PR no repositório)
- Ferramenta: adr-tools (CLI para gerenciar ADRs)
Trade-offs: CAP e PACELC
TEOREMA CAP (Brewer):
Em um sistema distribuído, durante uma partição de rede (P),
você só pode ter Consistência (C) ou Disponibilidade (A), não ambos.
┌─────────────────────────────────────┐
│ CAP │
│ │
│ C ────────── A │
│ \ / │
│ \ / │
│ \ / │
│ \ / │
│ \ / │
│ P │
│ │
│ CP: PostgreSQL, MongoDB (default) │
│ Prefere consistência, rejeita │
│ requests durante partição │
│ │
│ AP: Cassandra, DynamoDB │
│ Prefere disponibilidade, │
│ aceita dados stale │
│ │
│ CA: não existe em sistemas │
│ distribuídos reais │
└─────────────────────────────────────┘
PACELC (extensão do CAP):
Se há Partição (P): escolher entre Availability e Consistency (AC)
Else (E): escolher entre Latency e Consistency (LC)
Exemplos:
- PostgreSQL (primary-replica): PA/EC — durante partição perde A,
normalmente prioriza C (consistência forte)
- DynamoDB: PA/EL — durante partição mantém A (eventual consistency),
normalmente prioriza L (baixa latência)
- Cassandra: PA/EL — configurable consistency (QUORUM, ONE, ALL)
- CockroachDB: PC/EC — sempre consistente, aceita maior latência
NA PRÁTICA:
A maioria das aplicações web funciona bem com:
- Writes: consistência forte (transações ACID no primário)
- Reads: consistência eventual aceitável (ler de réplicas)
- Padrão: read-your-own-writes (ler do primário logo após escrever)
Documentação: C4 Model
C4 MODEL (Simon Brown) — 4 níveis de zoom:
NÍVEL 1 — SYSTEM CONTEXT:
"Quais sistemas existem e como se relacionam?"
┌─────────┐ ┌──────────────────┐ ┌──────────┐
│ Usuário │ ──→ │ E-commerce │ ──→ │ Stripe │
│ (pessoa)│ │ System │ │ (externo)│
└─────────┘ │ │ ──→ ├──────────┤
│ │ │ SendGrid │
└──────────────────┘ │ (externo)│
│ └──────────┘
↓
┌──────────────────┐
│ Analytics System │
│ (outro sistema) │
└──────────────────┘
NÍVEL 2 — CONTAINER (não Docker, mas componentes deployáveis):
"Dentro do sistema, quais são os containers?"
┌─────────────────────────────────────────────┐
│ E-commerce System │
│ │
│ ┌──────────┐ ┌──────────┐ ┌───────────┐ │
│ │ SPA │ │ API │ │ Worker │ │
│ │ (React) │→ │ (Node.js)│→ │ (Node.js) │ │
│ └──────────┘ └────┬─────┘ └─────┬─────┘ │
│ │ │ │
│ ┌─────┴──────┐ ┌─────┴─────┐ │
│ │ PostgreSQL │ │ RabbitMQ │ │
│ │ (Database) │ │ (Broker) │ │
│ └────────────┘ └───────────┘ │
└─────────────────────────────────────────────┘
NÍVEL 3 — COMPONENT:
"Dentro de um container, quais são os componentes?"
┌─────────────────────────────────────────┐
│ API (Node.js) │
│ │
│ ┌──────────────┐ ┌─────────────────┐ │
│ │ Auth │ │ Order │ │
│ │ Controller │ │ Controller │ │
│ └──────┬───────┘ └──────┬──────────┘ │
│ │ │ │
│ ┌──────┴───────┐ ┌──────┴──────────┐ │
│ │ Auth │ │ Order │ │
│ │ Service │ │ Service │ │
│ └──────┬───────┘ └──────┬──────────┘ │
│ │ │ │
│ ┌──────┴───────┐ ┌──────┴──────────┐ │
│ │ User │ │ Order │ │
│ │ Repository │ │ Repository │ │
│ └──────────────┘ └────────────────┘ │
└─────────────────────────────────────────┘
NÍVEL 4 — CODE (opcional, geralmente desnecessário):
Diagramas UML de classes. Gerado automaticamente do código.
FERRAMENTAS:
- Structurizr (do Simon Brown): DSL para C4, gera diagramas
- Mermaid: diagramas em Markdown (integra com GitHub)
- PlantUML: linguagem textual para diagramas
- draw.io/diagrams.net: ferramenta visual gratuita
- arc42: template de documentação de arquitetura
Fitness Functions e Arquitetura Evolutiva
FITNESS FUNCTIONS — testes automatizados para decisões arquiteturais:
"Se a arquitetura é importante, por que não testamos ela automaticamente?"
Exemplos de fitness functions:
// 1. ACOPLAMENTO — módulos não devem depender de internals de outros:
// Teste: verificar que módulo "orders" não importa de "users/internal"
test('Orders module não depende de internals de Users', () => {
const orderFiles = glob.sync('src/modules/orders/**/*.ts');
for (const file of orderFiles) {
const content = fs.readFileSync(file, 'utf-8');
expect(content).not.toMatch(/from ['"].*\/users\/internal/);
expect(content).not.toMatch(/from ['"].*\/users\/repositories/);
}
});
// 2. PERFORMANCE — latência p99 não deve regredir:
test('API p99 latency < 200ms', async () => {
const results = await runLoadTest({
url: 'https://staging.example.com/api/products',
duration: '60s',
rate: 100,
});
expect(results.latency.p99).toBeLessThan(200);
});
// 3. DEPENDÊNCIAS — limitar dependências transitivas:
test('Bundle size < 500KB', () => {
const stats = JSON.parse(fs.readFileSync('dist/stats.json', 'utf-8'));
const totalSize = stats.assets.reduce((sum, a) => sum + a.size, 0);
expect(totalSize).toBeLessThan(500 * 1024);
});
// 4. CAMADAS — controller não acessa repository diretamente:
test('Controllers não importam repositories diretamente', () => {
const controllerFiles = glob.sync('src/**/controllers/**/*.ts');
for (const file of controllerFiles) {
const content = fs.readFileSync(file, 'utf-8');
expect(content).not.toMatch(/Repository/);
}
});
// 5. COBERTURA — código crítico deve ter cobertura mínima:
// jest.config.js:
// coverageThreshold: {
// './src/modules/payments/': { branches: 90, functions: 95, lines: 95 },
// './src/modules/auth/': { branches: 85, functions: 90, lines: 90 },
// }
ARQUITETURA EVOLUTIVA (Neal Ford, Rebecca Parsons):
Princípios:
1. Mudanças incrementais: pequenas mudanças frequentes > grandes reescritas
2. Fitness functions: validar decisões automaticamente
3. Deployment pipelines: automatizar validação de cada mudança
4. Acoplamento adequado: módulos com interfaces claras e estáveis
Exemplo de evolução:
Monólito → Modular Monolith → Microsserviços (quando necessário)
Fase 1: Monólito (time de 3-5)
Simples, um deploy, transações ACID, sem overhead de rede.
Fase 2: Modular Monolith (time de 5-15)
Módulos com interfaces públicas, schemas separados, testes de contrato.
Fitness function: nenhum módulo acessa tabelas de outro módulo.
Fase 3: Extrair primeiro microsserviço (gargalo identificado)
O módulo que precisa escalar independentemente vira serviço.
Comunicação via eventos (event-driven) ou API.
Manter monólito para o resto — NÃO migrar tudo de uma vez.
Gestão de Dívida Técnica
DÍVIDA TÉCNICA (Ward Cunningham):
"Dívida técnica é como dívida financeira: acelera no curto prazo
mas paga juros enquanto não for quitada."
QUADRANTE DE DÍVIDA (Martin Fowler):
┌─────────────────┬─────────────────────────┐
│ │ Deliberada │
│ Prudente │ "Sabemos que isso │
│ │ não é ideal, mas │
│ │ precisamos entregar" │
│ │ → Tech debt planejada │
├─────────────────┼─────────────────────────┤
│ │ Deliberada │
│ Imprudente │ "Não temos tempo │
│ │ para design" │
│ │ → Preguiça/negligência │
├─────────────────┼─────────────────────────┤
│ │ Inadvertida │
│ Prudente │ "Agora que terminamos,│
│ │ sabemos como deveria │
│ │ ter sido feito" │
│ │ → Aprendizado natural │
├─────────────────┼─────────────────────────┤
│ │ Inadvertida │
│ Imprudente │ "O que é layered │
│ │ architecture?" │
│ │ → Falta de competência │
└─────────────────┴─────────────────────────┘
GESTÃO PRÁTICA:
1. IDENTIFICAR: code review, métricas (complexidade ciclomática,
cobertura, frequência de mudança, churn)
2. QUANTIFICAR: estimar custo de manter vs custo de refatorar
3. PRIORIZAR: usar Tech Debt Ratio = custo_remediar / custo_desenvolver
4. ALOCAR: dedicar 15-20% de cada sprint para reduzir dívida
5. PREVENIR: fitness functions, standards, code review
FERRAMENTAS DE ANÁLISE:
- SonarQube: análise estática, debt estimation
- CodeScene: análise comportamental (hotspots, churn)
- Code Climate: qualidade e manutenibilidade
- Métricas de código: complexidade ciclomática, fan-in/fan-out, instabilidade
Governança de Arquitetura
COMO GARANTIR QUE DECISÕES ARQUITETURAIS SÃO SEGUIDAS:
1. ADRs — documentar decisões e razões
Cada decisão significativa tem um ADR no repositório
Revisado pelo time em PR (como qualquer código)
2. FITNESS FUNCTIONS — validar automaticamente
Rodam no CI/CD como parte do pipeline
Se a fitness function falhar, o build falha
3. TECH RADAR — comunicar tecnologias adotadas/em avaliação
Adopt | Trial | Assess | Hold
Atualizado trimestralmente com o time
4. RFCS (Request for Comments) — propostas de mudança significativa
Documento público que o time inteiro pode comentar
Deadline para comentários, depois decisão documentada em ADR
5. ARCHITECTURAL KATAS — prática deliberada
Time resolve problemas de arquitetura fictícios
Desenvolve músculo de trade-off analysis
6. CODE REVIEW COM FOCO ARQUITETURAL:
Checklist:
□ Segue os padrões definidos nos ADRs?
□ Módulo acessa apenas interfaces públicas de outros módulos?
□ Dependências externas adicionadas estão no Tech Radar (Adopt/Trial)?
□ Testes de contrato atualizados para mudanças de API?
□ Performance: query N+1? Chamada síncrona desnecessária?
7. PLATFORM TEAM — time dedicado a:
- Manter templates de projetos (scaffolding)
- Prover ferramentas de observabilidade
- Gerenciar shared libraries e SDKs internos
- Definir e manter golden paths (caminho recomendado)
Referências e Fontes
- “Clean Architecture” — Robert C. Martin — princípios de arquitetura e a regra de dependência
- “Fundamentals of Software Architecture” — Mark Richards & Neal Ford — características arquiteturais, estilos e trade-offs
- “Building Evolutionary Architectures” — Ford, Parsons, Kua — fitness functions e arquitetura evolutiva
- “Software Architecture: The Hard Parts” — Ford, Richards et al. — decisões difíceis em arquiteturas distribuídas
- “Hexagonal Architecture” — Alistair Cockburn, 2005 — https://alistair.cockburn.us/hexagonal-architecture/
- C4 Model — Simon Brown — https://c4model.com/
- ADR GitHub — https://adr.github.io/ — templates e ferramentas para Architecture Decision Records
- ThoughtWorks Technology Radar — https://www.thoughtworks.com/radar