Desenhar e contratar: C4, sequência, eventos e OpenAPI
Quatro ferramentas que transformam uma lista de elementos em diagrama Mermaid validado e uma quinta que lê o contrato da API e devolve achados por severidade. Tudo sobre a plataforma de pedidos do marketplace, com a saída real de cada chamada.
12 min de leitura
Escolha como aprender
combine como quiserDica: o vídeo é um resumo visual rápido; o áudio e o texto trazem a aula completa. Terminar o áudio ou o vídeo, ou passar no quiz, já conta a aula como concluída.
No Módulo 1 você conectou o Moretes Architect MCP ao seu agente e viu que ele devolve cálculos e estruturas, nunca a opinião de um modelo. Nesta aula a gente usa isso para desenhar e contratar a plataforma de pedidos do marketplace: C4 em dois níveis, o diagrama de sequência do pedido com idempotência, o contrato do evento orders.OrderPlaced e uma revisão heurística da Orders API em OpenAPI. Ao final você sabe o que cada ferramenta produz, como ler a saída e o que continua sendo trabalho de linter ou de gente.
Por que nós explícitos vencem o desenho à mão
Peça a qualquer agente "desenhe a arquitetura em Mermaid" e ele desenha. O problema aparece na terceira iteração: o container que se chamava Orders API virou orders-svc num diagrama e OrderService no outro, uma seta aponta para uma caixa que não existe mais, e um rótulo com [ no meio quebra o render em silêncio. Ninguém percebe até a revisão de arquitetura, e aí o desenho vale menos que o quadro branco.
As três ferramentas de diagrama do servidor (c4_model, sequence_diagram, architecture_diagram) invertem a ordem: primeiro a lista, depois o traço. Toda caixa tem um id que precisa casar com a expressão ^[a-zA-Z][a-zA-Z0-9_]{0,39}$, e o servidor valida a lista antes de gerar uma linha:
- id duplicado falha com
Duplicate element id. Doisapino mesmo diagrama não existem. - aresta solta falha com a mensagem exata do que faltou. Eu testei apontar
buyer -> orderssem declararorderse recebi:Relation endpoint not declared: buyer -> orders. Use "system_Orders_platform" for the system itself at context level. - rótulo perigoso passa por
safeLabel, que troca por espaço os caracteres[ ] " < > { } | ; #, o acento grave e as quebras de linha, e corta em 160 caracteres. O texto que você escreveu vira texto; nunca vira sintaxe.
O custo é ter que listar os elementos antes de ver o desenho. Esse custo é o ganho: a lista é o modelo, o desenho é uma vista dele. Quando o agente erra, erra em uma mensagem de erro que ele mesmo lê e corrige, não em uma imagem bonita e inconsistente. Os limites são generosos para um sistema de 4 pessoas: 15 pessoas, 20 sistemas externos, 30 contêineres e 80 relações por diagrama.
c4_model: contexto primeiro, contêiner depois
c4_model tem dois níveis e é rígido sobre o que cabe em cada um. Em level: "context" entram só people e externalSystems; se você mandar containers, a resposta é Context level takes people and external systems; move containers to level=container. O próprio sistema ganha um id derivado do nome, system_ mais o nome com tudo que não é letra ou dígito trocado por _: para "Orders platform" é system_Orders_platform, e é esse id que as relações usam no nível de contexto.
Eu rodei o contexto da plataforma de pedidos com uma pessoa (buyer), um sistema externo (psp, o provedor de pagamento) e três relações. A saída tem elements: { people: 1, externalSystems: 1, containers: 0, relations: 3 }, unreferenced: [] e um campo nextLevel que diz literalmente o próximo passo: Call again with level=container and the containers inside the system boundary. Foi o que fiz.
No nível de contêiner entram os quatro de dentro da fronteira: Storefront (Next.js), Orders API (Node.js on Fargate), Orders DB (PostgreSQL) e Order events (EventBridge), com seis relações. O Mermaid que volta está no bloco abaixo, expandido: a pessoa vira um nó arredondado, o sistema vira um subgraph, o externo vira caixa dupla, e as duas classDef no fim pintam pessoa e externo. Repare em unreferenced: qualquer elemento que não aparece em nenhuma relação é listado ali. Ou você esqueceu a seta, ou a caixa não pertence a esse diagrama.
Como ver o desenho. Claude Code, Codex e Kiro mostram o texto Mermaid na resposta. Se você pedir ao agente que grave num arquivo .md dentro de um bloco ``` `mermaid ``, o preview do VS Code (com a extensão de Mermaid) e o GitHub renderizam na hora. Sem nada disso, cole o texto em mermaid.live e o diagrama aparece. O resultado vem com kind: "heuristic-design-aid" e validated: false`: a ferramenta validou o grafo, não a arquitetura.
flowchart TB
buyer(["Buyer<br/>Places and tracks orders"])
subgraph system_Orders_platform["Orders platform"]
web["Storefront [Next.js]"]
api["Orders API [Node.js on Fargate]"]
db["Orders DB [PostgreSQL]"]
bus["Order events [EventBridge]"]
end
psp[["Payment provider [REST + webhooks]"]]
buyer -->|"uses"| web
web -->|"calls (HTTPS/JSON)"| api
api -->|"reads and writes"| db
api -->|"authorizes payment"| psp
psp -->|"webhook: payment captured"| api
api -->|"publishes OrderPlaced"| bus
classDef person fill:#1f3b63,stroke:#5b9bd5,color:#fff
class buyer person
classDef external fill:#4a4a4a,stroke:#9a9a9a,color:#fff
class psp externalsequence_diagram e architecture_diagram: o fluxo crítico e a vista rápida
O C4 mostra quem fala com quem. Ele não mostra em que ordem, e ordem é onde mora o bug de pagamento. sequence_diagram recebe participants (2 a 15, cada um com id, label e kind actor ou participant) e messages (1 a 60) com quatro estilos de seta: sync vira ->>, async vira -), reply vira -->> e reply-async vira --). Cada mensagem aceita activate, uma note e um block do tipo alt, else, loop, opt, par ou end. A validação é a mesma filosofia: mensagem para participante não declarado falha com Unknown participant in message, else sem alt aberto falha, e bloco que você abriu e esqueceu de fechar é fechado no fim.
O exemplo que uso é "Place order with idempotency": seis mensagens entre Storefront, Orders API, Orders DB e provedor de pagamento. A saída (bloco abaixo) sai com title, autonumber e o deactivate api no final, porque a primeira mensagem tinha activate: true. O que o desenho deixa visível, e o C4 não: o INSERT order status=pending acontece antes de authorize(amount, orderId). Se a API cai entre os dois, existe um pedido pendente no banco, e é o Idempotency-Key da primeira linha que permite ao retry do Storefront encontrar esse pedido em vez de criar outro. A ferramenta é honesta sobre o que isso é: limitation: "Illustrates the intended interaction; it is not a trace of a real system."
architecture_diagram é a ferramenta de 20 segundos. Recebe nodes (1 a 40, id e label) e edges (até 80, from, to, label opcional) e devolve um flowchart LR. Só uma validação: Duplicate node or unknown edge endpoint. No exemplo da plataforma são cinco nós (Storefront, Orders API, OrderPlaced queue, Billing, Inventory) e quatro arestas, duas delas sem rótulo. Não tem pessoa, não tem fronteira de sistema, não tem tecnologia. Se você precisa disso, é c4_model; se precisa só mostrar que a fila alimenta dois consumidores, é esta.
sequenceDiagram
title Place order with idempotency
autonumber
participant web as Storefront
participant api as Orders API
participant db as Orders DB
participant psp as Payment provider
web->>api: POST /orders (Idempotency-Key)
activate api
api->>db: INSERT order status=pending
api->>psp: authorize(amount, orderId)
psp-->>api: authorized
api->>db: UPDATE status=authorized
api-->>web: 201 Created
deactivate apievent_contract: orders.OrderPlaced com envelope CloudEvents
A seta publishes OrderPlaced do C4 é uma promessa para três times que você não controla: billing, inventory e notifications. event_contract transforma essa seta em contrato. A entrada é pequena: eventName em PascalCase pontuado (orders.OrderPlaced; a regex rejeita order_placed), producer, a lista de consumers, os payloadFields com type entre string, number, integer, boolean, timestamp, object, array, id e money (cada campo com required, padrão true, e pii, padrão false), mais ordering (none, per-key, global; padrão per-key) e deliveryGuarantee (at-least-once, at-most-once, effectively-once).
A saída tem quatro partes que eu leio nesta ordem:
- Envelope.
CloudEvents 1.0 attributes (id, type, source, time, subject, dataschema) plus traceparent. Oidé a chave de deduplicação do consumidor, osubjecté a chave de partição, otraceparentliga o evento ao trace W3C de quem publicou. - JSON Schema (draft 2020-12). O campo
total, declarado comomoney, vira um objeto comamountdo tipo string, descriçãoDecimal as string; never float, ecurrencycom padrão^[A-Z]{3}$. Centavos em float é o erro mais caro de corrigir depois de um ano de eventos gravados. - Rules. Idempotência:
Consumers keep an idempotency store keyed by event id (TTL >= max redelivery window). Ordenação per-key:Partition by subject; a single consumer per partition preserves order per key only. Evolução em quatro linhas: só mudança aditiva dentro da versão maior, quebra vira novo sufixo detypeou nova versão dedataschemacom publicação dupla, consumidor ignora campo desconhecido, nunca reusar nome com outro significado. Falha: DLQ depois de retries limitados com o evento original e os metadados do erro, replay é operação autorizada com janela de tempo e consumidor alvo, nunca reenvio cego, mensagem venenosa vai para quarentena. - Warnings. Duas no exemplo: a de money e a de PII, porque
customerEmailveio marcadopii: true:consider referencing by id instead of embedding, define retention in the broker and in every consumer. Com mais de 5 consumidores aparece uma terceira pedindo um registro de consumidores com dono e contato.
Cada consumidor volta com mustDefine: tratamento da chave de idempotência, política de retry e DLQ, versão de schema tolerada e dono. É um template determinístico: ele não sabe se o seu broker é EventBridge, SNS ou Kafka, e por isso termina com nextTools: architecture_diagram, failure_rehearsal, decision_record.
{
"eventName": "orders.OrderPlaced",
"envelope": "CloudEvents 1.0 attributes (id, type, source, time, subject, dataschema) plus traceparent",
"jsonSchema": {
"required": ["id", "type", "specversion", "source", "time", "data"],
"properties": {
"data": {
"required": ["orderId", "customerId", "total", "placedAt"],
"properties": {
"total": {
"type": "object",
"required": ["amount", "currency"],
"properties": {
"amount": { "type": "string", "description": "Decimal as string; never float" },
"currency": { "type": "string", "pattern": "^[A-Z]{3}$" }
}
},
"customerEmail": { "type": "string" }
}
}
}
},
"rules": {
"idempotency": "Consumers keep an idempotency store keyed by event id (TTL >= max redelivery window) and make side effects conditional.",
"ordering": "Partition by subject; a single consumer per partition preserves order per key only.",
"evolution": [
"Additive changes only within a major version (new optional fields)",
"Breaking change = new type suffix or dataschema major version, dual-publish during migration",
"Consumers ignore unknown fields",
"Never reuse a field name with a different meaning"
],
"failure": [
"Dead-letter after bounded retries with the original event and error metadata",
"Replay is an authorized operation with a time window and a target consumer, never a blind re-send",
"Poison messages are quarantined, not dropped silently"
]
},
"warnings": [
"Money fields (total) use decimal strings plus currency; floats are rejected.",
"PII fields (customerEmail): consider referencing by id instead of embedding, define retention in the broker and in every consumer."
],
"consumers": [
{ "consumer": "billing", "mustDefine": ["idempotency key handling", "retry and DLQ policy", "schema version tolerated", "owner"] }
],
"nextTools": ["architecture_diagram", "failure_rehearsal", "decision_record"]
}openapi_review: a Orders API sob 12 checagens, e o que fica para o linter
openapi_review recebe um único argumento, document, que é o JSON OpenAPI 3.x inteiro (2.0 falha com OpenAPI 3.x document required; o teto é 300 paths). Pela CLI você passa o arquivo cru: node dist/cli.js call openapi_review --input orders-api.json --format sarif --fail-on medium. O documento da Orders API que eu usei tem 2 paths e 4 operações: GET e POST /orders, GET e DELETE /orders/{id}, com um scheme bearer e security global.
O summary voltou { high: 0, medium: 2, info: 10 }. Os dois medium estão no mesmo lugar, GET /orders: não documenta nenhuma resposta 4xx nem default, e é uma coleção sem parâmetro de paginação (Collection GET without pagination parameters; unbounded responses grow with data). Com 300 pedidos por minuto no pico, esse endpoint devolve 432 mil itens por dia dentro de um mês de operação; paginação não é estilo, é mudança no formato da resposta, e por isso é medium. Entre os info: POST /orders sem header Idempotency-Key (retries may duplicate side effects, exatamente o que a sequência da seção anterior resolve), DELETE /orders/{id} sem If-Match/ETag nem 409/412 (concurrent writes can overwrite each other silently), nenhuma das quatro operações documenta 429, e nenhuma tem summary ou description. Um high apareceria se não houvesse securitySchemes ou se um servers[].url fosse http://; aqui não houve.
O que não está nessa lista é tão importante quanto: convenção de nomes, casing de campos, qualidade do texto das descrições, completude dos schemas. Isso é trabalho de linter, e a própria resposta diz: Static subset of checks; use Spectral or vendor linters for style rules. As outras duas limitations são Cannot evaluate authorization logic or runtime behavior e Empty findings is not an API approval. Leia a segunda duas vezes antes de levar um zero achado para a reunião.
O prompt api_design_review. O servidor publica 20 prompts, e este é o fluxo de revisão de API. O texto é curto e vale copiar: Ask for the OpenAPI document and the consumers. Run openapi_review, then sequence_diagram for the two most critical flows and latency_budget for the end-to-end objective. For events use event_contract. Findings need owner and validation; style rules belong to a linter. No Claude Code ele aparece como comando /mcp__moretes-architect__api_design_review; em outros clientes, no menu de prompts. Ele encadeia três das ferramentas desta aula e uma da próxima (latency_budget), e termina na frase que eu mais repito em review: achado sem dono e sem validação é só uma linha de JSON.
{
"kind": "openapi-heuristic-review",
"openapi": "3.1.0",
"paths": 2,
"operations": 4,
"securitySchemes": ["bearer"],
"findings": [
{ "severity": "medium", "path": "GET /orders", "message": "No 4xx or default error response documented." },
{ "severity": "medium", "path": "GET /orders", "message": "Collection GET without pagination parameters; unbounded responses grow with data." },
{ "severity": "info", "path": "POST /orders", "message": "POST without an Idempotency-Key header; retries may duplicate side effects." },
{ "severity": "info", "path": "DELETE /orders/{id}", "message": "Mutation without If-Match/ETag or 409/412; concurrent writes can overwrite each other silently." },
{ "severity": "info", "path": "GET /orders", "message": "No 429 response; document rate limiting behavior." }
],
"summary": { "high": 0, "medium": 2, "info": 10 },
"limitations": [
"Static subset of checks; use Spectral or vendor linters for style rules",
"Cannot evaluate authorization logic or runtime behavior",
"Empty findings is not an API approval"
]
}Cada ferramenta e o artefato que ela produz
Toque num conceito e depois na definição.