Capítulo 4 . Arquitetura

Arquitetura de Software

As decisões que custam caro para mudar.

Canal Sandeco
Definição

O que é arquitetura
de software?

É o conjunto de decisões estruturais que moldam o sistema inteiro. As escolhas que, uma vez feitas, ficam caras de desfazer.

Faceta 1

Estrutura

Os componentes do sistema, suas responsabilidades e como se relacionam. O mapa de quem fala com quem.

Faceta 2

Decisões duráveis

Aquilo que é difícil de mudar depois. Linguagem, banco, fronteiras de serviço, modelo de concorrência: o esqueleto do sistema.

Faceta 3

Princípios

As regras que guiam a evolução do sistema. O que pode entrar, o que precisa sair, como cada parte pode crescer sem quebrar o todo.

"

Arquitetura é sobre o que é importante. Seja lá o que isso for.

Ralph Johnson Coautor do livro Design Patterns (Gang of Four).

Padrões resolvem problemas dentro de um módulo. Arquitetura responde como os módulos existem e por que.

Arquitetura 1 de 5

Monolito

Um processo. Um deploy. Tudo no mesmo lugar.

Sistema novo

Time pequeno

Domínio em descoberta

A Solução

Um processo. Um deploy. Um banco.

Os módulos conversam por chamada de função. Nada de rede no caminho.

Modular por dentro, único por fora

Cada módulo tem responsabilidade clara, mas tudo compila e implanta junto.

Chamadas locais

Função chama função. Sem JSON, sem timeout.

Transação única

Commit e rollback no mesmo banco.

Refactor barato

IDE move código, type-check pega quebra.

Em código, parece com isto.

Antes, dez repositórios e configs de rede. Depois, um repo com módulos coesos.

Microservices prematuro
# 5 repositórios, 5 Dockerfiles, 5 pipelines
auth-service/
  Dockerfile
  k8s/deployment.yaml
  src/main.py
user-service/
  Dockerfile
  k8s/deployment.yaml
  src/main.py
order-service/   ...
payment-service/ ...
billing-service/ ...

# E no código:
def create_order(req):
    user = http.get("http://user-svc/u/" + req.uid)
    bill = http.post("http://bill-svc/charge", ...)
    # retry, timeout, circuit breaker, log corr.

Cada chamada simples virou HTTP, fila ou RPC. Erros distribuídos, observabilidade fragmentada, deploy desencontrado.

Monolito modular
# 1 repo, 1 Dockerfile, 1 pipeline
app/
  Dockerfile
  src/
    auth/
    users/
    orders/
    payments/
    billing/
  main.py

# E no código:
def create_order(req):
    user = users.find(req.uid)
    billing.charge(user, req.amount)
    return orders.save(req)
# Chamada de função. Transação local.

Os mesmos módulos existem, mas vivem no mesmo processo. Refactor é mover pasta. Erro de tipo é compilação, não outage.

Quando começar com Monolito

Três perguntas. Se as três forem sim, comece aqui.

1

Você está começando um sistema do zero?

Sem usuários, sem histórico, sem fronteiras de domínio testadas pela realidade.

2

O time é pequeno, com menos de dez pessoas?

Não há gente sobrando para manter pipelines, observabilidade e infra de N serviços.

3

O domínio ainda está sendo descoberto?

As fronteiras do problema vão mudar várias vezes antes de estabilizar.

Cuidado: monolito não é desorganizado. Mantenha módulos coesos, fronteiras claras e dependências unidirecionais. Assim, quando o domínio amadurecer, você poderá quebrar em pedaços que fazem sentido.

Arquitetura 2 de 5

Camadas

Cada responsabilidade no seu andar.

Apps web e APIs

Domínio com regras

Trocas isoladas

O Problema

Tudo conhece tudo.

HTTP, regra de negócio, SQL e e-mail moram na mesma classe. Qualquer mudança vaza por todo lado.

Dependências em todas as direções

A UI fala com o banco. O banco volta para a UI. A regra de negócio depende do framework. Sem hierarquia.

Sem hierarquia

Cada parte fala com qualquer outra.

Trocas custam caro

Mudou o banco, quebrou a UI.

Testar dói

Regra de negócio amarrada a HTTP e SQL.

A Solução

Andares. Dependência em uma direção só.

Apresentação, aplicação, domínio, infraestrutura. Cada andar conhece só o de baixo.

Quatro andares, uma escada

O request desce, executa a regra, salva. A resposta sobe. Nenhuma camada pula degrau.

Domínio limpo

Regra de negócio sem framework.

Trocas locais

Outro banco, outra UI, mesmo domínio.

Teste por camada

Cada andar isolado em mock simples.

Trocas Vivas

Troque a UI. Troque o banco. O domínio não muda.

A apresentação vai e vem. A infraestrutura vai e vem. O domínio fica.

Bordas plugáveis, miolo fixo

A camada de cima e a de baixo trocam de implementação. A regra de negócio nem percebe.

UI plugável

REST, GraphQL, CLI: mesma regra.

Banco plugável

Postgres, Mongo, Redis: mesmo contrato.

Domínio intacto

Zero alterações ao trocar a borda.

Em código, parece com isto.

Antes, um arquivo faz tudo. Depois, cada arquivo cuida do seu andar.

Sem camadas
@app.route("/orders", methods=["POST"])
def create_order():
    data = request.json
    if data["amount"] < 0:
        return "<h1>Valor inválido</h1>"
    conn.execute(
        "INSERT INTO orders ...",
        data["user_id"], data["amount"]
    )
    smtp.send(data["email"], "Pedido criado")
    return render_template("ok.html")
# HTTP, regra, SQL, e-mail e UI juntos.

A rota carrega HTTP, validação, SQL, e-mail e renderização. Qualquer mudança em qualquer concern vaza para os outros.

Com camadas
# presentation
@app.route("/orders", methods=["POST"])
def create_order():
    result = use_case.execute(request.json)
    return jsonify(result)

# application
class PlaceOrderUseCase:
    def execute(self, dto):
        order = Order.new(dto.amount)
        self.repo.save(order)

# domain
class Order:
    def validate(self): ...

# infrastructure
class PostgresOrderRepo:
    def save(self, order): ...

Cada andar tem uma responsabilidade só. Mudar UI ou banco mexe num arquivo. O domínio fica intacto.

Quando aplicar Camadas

Três perguntas. Se as três forem sim, use.

1

Existem regras de negócio que valem por si, fora de framework e banco?

Cálculos, validações, fluxos de domínio que continuariam corretos em qualquer UI ou storage.

2

Você quer trocar UI ou banco sem reescrever o negócio?

REST hoje, GraphQL amanhã. Postgres hoje, Mongo amanhã. O domínio fica imune.

3

O time vai testar e evoluir cada parte separadamente?

Testar a regra sem banco. Trocar a infra sem mexer no negócio. Refactor com confiança.

Cuidado: em scripts ou CRUDs minúsculos, camadas viram boilerplate sem ganho. Use quando o domínio merecer o investimento.

"

Camadas separam o que muda devagar do que muda o tempo todo. Cada andar conhece só o de baixo.

Dependência em uma direção Próxima arquitetura: SOA.
Arquitetura 3 de 5

SOA

Service-Oriented Architecture. Serviços compartilhados, um barramento que conecta.

Múltiplos canais

Integrar legados

Reúso entre apps

O Problema

Integração ponto-a-ponto vira teia.

Cada cliente fala diretamente com cada serviço. N clientes vezes M serviços de fios para manter.

Spaghetti de integrações diretas

Cada app guarda os endereços de cada serviço. Cada serviço novo obriga cada cliente a se atualizar.

Acoplamento forte

Cada cliente conhece cada serviço.

Mudança vaza

Trocar endpoint quebra N clientes.

Reúso difícil

Regras repetidas em cada cliente.

A Solução

Um barramento, muitos serviços.

Os consumidores conhecem só o ESB. Ele recebe, decide para onde vai, encaminha.

Enterprise Service Bus no centro

Recebe a mensagem, identifica o destino por regra de negócio, encaminha para o serviço certo.

ESB orquestra

Roteia, transforma, valida no caminho.

Serviços stateless

Cada chamada vive por si.

Reúso real

Web, mobile e desktop chamam o mesmo serviço.

Em código, parece com isto.

Antes, cada cliente fala HTTP com cada serviço. Depois, todos falam com o barramento.

Sem SOA
# Web App
patient = http.get("http://patient-svc/buscar/" + id)
appt    = http.post("http://consult-svc/agendar", ...)
exam    = http.post("http://exam-svc/solicitar", ...)

# Mobile App (outro time)
patient = http.get("http://patient-svc/buscar/" + id)
exam    = http.post("http://exam-svc/solicitar", ...)

# Desktop App (legado)
soap.call("PatientSrv.find", id)
# Cada cliente conhece cada endpoint.
# Mudou o endpoint? Atualiza tudo.

Cada app carrega os endereços e contratos de cada serviço. Mudou um endpoint, três apps precisam ser atualizados.

Com SOA
# Web, Mobile, Desktop: todos só conhecem o ESB
result = esb.send(Message(
    operation="BuscarPaciente",
    payload={"id": id}
))

# O ESB decide o destino pela operação
class Esb:
    def send(self, msg):
        endpoint = self.routes[msg.operation]
        return endpoint.handle(msg.payload)

# Trocou o serviço? Muda 1 linha do roteador.

Os consumidores falam uma única língua de mensagens. O ESB conhece todos os serviços e decide para onde encaminhar.

Quando aplicar SOA

Três perguntas. Se as três forem sim, use.

1

Várias aplicações precisam consumir as mesmas funcionalidades?

Web, mobile, desktop, parceiros externos: todos querem buscar paciente, agendar consulta, solicitar exame.

2

Você precisa integrar sistemas heterogêneos ou legados?

Mainframe falando SOAP, sistema novo em REST, parceiro em XML. O ESB traduz no meio.

3

Faz sentido centralizar regras de negócio em serviços reutilizáveis?

Calcular preço, validar CPF, conferir estoque: uma lógica, vários consumidores.

Cuidado: o ESB centraliza poder. Vira gargalo de performance e de governança se for grande demais. Por isso microservices surgiram como evolução, distribuindo a inteligência.

"

SOA centraliza o roteamento para descentralizar as funcionalidades. Um idioma comum no barramento, muitos serviços por trás.

Um endpoint conhecido, N serviços plugáveis Próxima arquitetura: Microservices.
Arquitetura 4 de 5

Microservices

Serviços pequenos, independentes, donos dos próprios dados.

Times autônomos

Stacks diversas

Escala por componente

O Problema

Monolito grande, time inteiro na fila.

Um deploy de cada vez. Uma quebra derruba todo mundo. Uma feature precisa escalar, o sistema inteiro escala junto.

Acoplamento de deploy

Quatro times, uma única pipeline de produção. Cada deploy é um stop-the-world.

Deploy bloqueia

Um time entra, os outros esperam.

Falha global

Bug em uma feature, sistema todo cai.

Escala em bloco

Pico em pedidos, sobe o monolito inteiro.

A Solução

Decomposição por domínio. Cada um na sua.

Cada microsserviço com seu time, seu deploy, seu banco. API Gateway na frente.

Distribuído por escolha, não por moda

Cada serviço é dono do seu domínio e do seu dado. Conversa por API ou mensageria. Escala por demanda própria.

Banco por serviço

Cada um manda no seu dado.

Deploy independente

Cada time libera no seu ritmo.

Escala por serviço

Pico em pedidos, sobe só pedidos.

Em código, parece com isto.

Antes, tudo num pacote. Depois, cada domínio com seu serviço, seu repo, sua stack.

Monolito
# 1 repo, 1 pipeline, 1 banco
app/
  src/
    menu/
    orders/
    payment/
    analytics/
  main.py
  Dockerfile

# todos no mesmo Python, mesmo Postgres
def place_order(req):
    menu.load(req.item)
    payment.charge(req.user, req.total)
    analytics.track(req)
    orders.save(req)
# Bug em analytics? Pedido cai junto.

Todos os domínios compartilham processo, banco e linguagem. Qualquer falha vira incidente geral. Qualquer pico exige escalar tudo.

Microservices
# 4 repos, 4 pipelines, 4 bancos
menu-svc/      # Python + Postgres
orders-svc/    # Go + Postgres (com replicas)
payment-svc/   # Java + Postgres
analytics-svc/ # Python + ClickHouse

# Comunicação via API Gateway
async def place_order(req):
    item    = await http.get("menu-svc/items/" + req.item)
    paid    = await http.post("payment-svc/charge", ...)
    bus.publish("order.placed", req)
# Cada serviço sobe, cai e escala sozinho.

Cada microsserviço tem repo, banco e stack próprios. O API Gateway é a porta pública. Mensageria desacopla quem reage.

Quando aplicar Microservices

Três perguntas. Se as três forem sim, considere.

1

O domínio já tem fronteiras claras, testadas pela operação?

Você sabe onde corta menu, pedidos, pagamento. Os limites foram descobertos rodando o monolito antes.

2

Times diferentes querem evoluir em ritmos diferentes?

Pagamento libera duas vezes por semana. Analytics libera duas vezes por dia. Cada um quer sua pipeline.

3

Existe necessidade real de escalar partes separadamente?

Pedido escala em pico do almoço, analytics em rollup noturno. Cada um com seu autoscaling.

Cuidado: microsserviços trocam complexidade local por complexidade distribuída. Observabilidade, tracing, transações distribuídas, consistência eventual e governança viram custo permanente. Comece no monolito modular e quebre quando a dor justificar.

"

Microservices é evolução, não início. Comece no monolito, divida quando o domínio pedir.

Pequenos, independentes, donos dos dados Próxima arquitetura: Event-Driven.
Arquitetura 5 de 5

Event-Driven

Algo acontece. Quem precisar, reage.

Reações em tempo real

Pub/Sub assíncrono

Trilha de auditoria

O Problema

Cadeia síncrona. Tudo espera tudo.

Producer chama Email, espera. Email chama Invoice, espera. Cada nova reação edita o producer.

Acoplamento temporal e de código

O producer carrega a lista de consumidores. A latência total é a soma de todos. Falha em um trava o resto.

Latência acumula

120 + 200 + 90 + 150 ms . tudo somado.

Falha em cadeia

Um consumer cai, o resto não roda.

Producer acoplado

Reação nova obriga editar o producer.

A Solução

Publica e segue. Quem quiser, escuta.

Producer emite um evento ao broker e segue. Cada consumidor lê no seu tempo, em paralelo.

Broker no meio, consumidores plugáveis

Eventos imutáveis em um tópico. Cada consumidor lê do jeito que quiser, no ritmo que aguentar. Adicionar reação não toca o producer.

Async não bloqueia

Producer responde sem esperar reações.

N consumidores

Mesmo evento, várias reações independentes.

Producer indiferente

Consumidor novo, zero código no emissor.

Em código, parece com isto.

Antes, o producer conhece e chama cada consumer. Depois, ele só publica um fato.

Síncrono em cadeia
class OrderService:
    def place_order(self, order):
        self.repo.save(order)

        # cadeia síncrona de reações
        email.send(order)        # 120 ms
        invoice.create(order)    # 200 ms
        stock.reserve(order)     # 90  ms
        analytics.track(order)   # 150 ms

        return order

# Reação nova? Editar place_order de novo.
# Falha em invoice? place_order falha.

O producer conhece, chama e espera cada consumer. Latências se somam, falhas viram cascata, novas reações reabrem o emissor.

Event-Driven
class OrderService:
    def place_order(self, order):
        self.repo.save(order)
        bus.publish("order.placed", order)
        return order

# cada consumer assina no seu tempo
@on("order.placed")
def send_email(order): ...

@on("order.placed")
def create_invoice(order): ...

# Reação nova? Novo handler. Producer intacto.

O producer publica o fato e retorna. Consumidores assinam o tópico e reagem em paralelo. Falha local fica local. Novo consumer não toca o emissor.

Quando aplicar Event-Driven

Três perguntas. Se as três forem sim, use.

1

Várias partes precisam reagir ao mesmo fato?

Pedido confirmado dispara e-mail, nota fiscal, estoque, analytics, fidelidade. E provavelmente mais coisas amanhã.

2

As reações podem ser assíncronas?

O cliente não precisa esperar o e-mail sair, nem a nota gerar. Cada reação tem seu próprio SLA.

3

Você ganha com auditoria e replay dos eventos?

Ter o log imutável de tudo que aconteceu permite reprocessar, depurar e construir novas visões a partir do passado.

Cuidado: consistência vira eventual. Debug fica distribuído. Schema de evento evolui e quebra consumidores. Garanta versionamento, observabilidade e idempotência antes de adotar.

"

Event-Driven não é sobre mensagens. É sobre fatos que vivem, sendo lidos por quem se importa.

Producer publica. Consumidores escutam. Os cinco estilos arquiteturais, completos.
Canal Sandeco