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.
Estrutura
Os componentes do sistema, suas responsabilidades e como se relacionam. O mapa de quem fala com quem.
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.
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.
Monolito
Um processo. Um deploy. Tudo no mesmo lugar.
Sistema novo
Time pequeno
Domínio em descoberta
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.
# 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.
# 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.
Você está começando um sistema do zero?
Sem usuários, sem histórico, sem fronteiras de domínio testadas pela realidade.
O time é pequeno, com menos de dez pessoas?
Não há gente sobrando para manter pipelines, observabilidade e infra de N serviços.
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.
Camadas
Cada responsabilidade no seu andar.
Apps web e APIs
Domínio com regras
Trocas isoladas
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.
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.
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.
@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.
# 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.
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.
Você quer trocar UI ou banco sem reescrever o negócio?
REST hoje, GraphQL amanhã. Postgres hoje, Mongo amanhã. O domínio fica imune.
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.
SOA
Service-Oriented Architecture. Serviços compartilhados, um barramento que conecta.
Múltiplos canais
Integrar legados
Reúso entre apps
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.
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.
# 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.
# 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.
Várias aplicações precisam consumir as mesmas funcionalidades?
Web, mobile, desktop, parceiros externos: todos querem buscar paciente, agendar consulta, solicitar exame.
Você precisa integrar sistemas heterogêneos ou legados?
Mainframe falando SOAP, sistema novo em REST, parceiro em XML. O ESB traduz no meio.
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.
Microservices
Serviços pequenos, independentes, donos dos próprios dados.
Times autônomos
Stacks diversas
Escala por componente
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.
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.
# 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.
# 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.
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.
Times diferentes querem evoluir em ritmos diferentes?
Pagamento libera duas vezes por semana. Analytics libera duas vezes por dia. Cada um quer sua pipeline.
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.
Event-Driven
Algo acontece. Quem precisar, reage.
Reações em tempo real
Pub/Sub assíncrono
Trilha de auditoria
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.
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.
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.
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.
Várias partes precisam reagir ao mesmo fato?
Pedido confirmado dispara e-mail, nota fiscal, estoque, analytics, fidelidade. E provavelmente mais coisas amanhã.
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.
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.