---
title: "O padrão Saga é sobre tempo, não sobre serviços"
url: https://tiarebalbi.com/pt-br/blog/saga-pattern-time-not-services
markdown: https://tiarebalbi.com/pt-br/blog/saga-pattern-time-not-services.md
description: "O padrão Saga nasceu para transações longas dentro de um banco. O Bullpen precisa dele para execuções parciais e cancelamentos, não para um novo serviço."
author: "Tiarê Balbi Bonamini"
locale: pt-br
published: 2026-10-26
updated: 2026-10-26
category: "Engineering"
translation: https://tiarebalbi.com/en/blog/saga-pattern-time-not-services
---
# O padrão Saga é sobre tempo, não sobre serviços

No Bullpen, toda compra passa pelos mesmos quatro passos: reservar o poder de compra, entregar a ordem à bolsa, registrar cada execução quando ela chega e liberar o que a ordem não usou. O problema começa quando esses passos deixam de chegar em ordem.

Uma ordem de compra no Bullpen pede 100 ações. Quarenta são executadas. O jogador clica em cancelar. Uma segunda execução, de 25, já tinha acontecido quando o cancelamento saiu, mas a mensagem dela chega ao Bullpen depois que a ordem já mostra "cancelamento solicitado". O que o ledger diz agora?

Nenhuma transação de banco consegue responder isso, porque nenhuma transação dura o suficiente para ver isso acontecer. A ordem vive por minutos; uma transação deveria viver por milissegundos. Esse é todo o motivo de a Parte 4 precisar do padrão Saga, e o motivo é o tempo, não um novo serviço nem um segundo banco. Tempo aqui quer dizer commits separados, não uma duração mínima: uma saga entre dois serviços pode terminar em milissegundos e ainda assim ser necessária. O que obriga a ordem do Bullpen a ter commits separados é que ela espera o mundo, e o mundo leva minutos.

O Bullpen continua mantendo cada passo de uma ordem dentro de uma transação atômica no schema `trading`, como a Parte 3 decidiu. O que muda é que agora uma ordem atravessa várias dessas transações, e cada uma precisa de um caminho de volta definido, ou de um caminho para frente definido quando não existe volta.

Esta parte segue o mesmo histórico de decisões da Parte 3, mas só as três decisões que sustentam o argumento recebem o tratamento completo. O resto fica numa tabela.

## Por que o padrão Saga estava no plano desde a primeira semana

A saga é mais antiga que os microsserviços. Hector Garcia-Molina e Kenneth Salem a descreveram no [artigo do SIGMOD de 1987](https://dl.acm.org/doi/10.1145/38713.38742) para um problema dentro de um único banco: transações de longa duração que seguram locks por tanto tempo que todo o resto espera. A resposta deles foi quebrar a transação longa em uma sequência de transações curtas, cada uma com seu próprio commit, cada uma com uma transação de compensação que desfaz o efeito dela se um passo posterior falhar. O banco nunca precisa manter tudo aberto.

O sentido de microsserviços veio décadas depois. O [padrão Saga](https://microservices.io/patterns/data/saga.html) de Chris Richardson usa a mesma ideia para manter dados consistentes entre serviços que têm cada um o seu banco, e é a versão que a maioria dos engenheiros aprendeu. Foi também a versão que me fez colocar uma saga de ordens no plano do Bullpen na primeira semana: o ADR-0002 a citou como um split que teria de se justificar, a Parte 3 adiou o ciclo de vida da ordem para esta parte, e a arquitetura planejada da Parte 4 ainda mostra um nó Order Saga, um motor de workflow separado ao lado do app de trading.

Eu esperava que a saga fosse o primeiro split de verdade no Bullpen. Reler o artigo original mudou essa expectativa antes de qualquer código rodar. O problema de 1987 é o problema do Bullpen: um processo de negócio que vive mais que uma transação. Se ele também precisa de um novo runtime é outra pergunta, e a Decisão 1 é onde a construção responde.

## O que uma transação não consegue segurar

A [decisão 3 da Parte 3](https://tiarebalbi.com/pt-br/blog/database-per-service-free-tier-data-ownership) colocou a ordem, seus lançamentos no ledger e a reserva de poder de compra em uma transação. Isso continua valendo para cada mudança individual. Não vale para o ciclo de vida, por três motivos simples. Uma Vercel Function no Hobby para depois de 300 segundos. Uma transação no Postgres deixada aberta segura seus locks de linha enquanto viver, e o snapshot antigo dela impede o vacuum de limpar atrás de todas as outras escritas. E uma execução não é algo que o Bullpen decide; ela chega, às vezes em pedaços, às vezes atrasada.

Então a ordem vira uma pequena máquina de estados, escrita no ADR-0017. As regras, o registro de decisão e os testes que as verificam entraram no mesmo commit. Escrevi as regras primeiro enquanto trabalhava, mas o histórico não mostra um commit só com as regras, então não vou afirmar mais do que ele mostra.

| Estado | Entra quando | Pode ir para |
|---|---|---|
| aceita | A ordem é enviada; o poder de compra é reservado com um teto 5% acima do preço, na mesma transação | em andamento, parcialmente executada, executada, cancelamento solicitado, rejeitada |
| em andamento | O sweep entregou a ordem à bolsa | parcialmente executada, executada, cancelamento solicitado |
| parcialmente executada | Uma execução é registrada e ainda resta quantidade em aberto | parcialmente executada, executada, cancelamento solicitado |
| cancelamento solicitado | O jogador pediu para cancelar; a bolsa não confirmou, então execuções ainda podem chegar | cancelamento solicitado (uma execução é registrada), cancelada, executada |
| cancelada | A bolsa confirmou o cancelamento com um horário efetivo | cancelada (uma execução atrasada é registrada), executada |
| executada | As execuções registradas completam a quantidade, inclusive depois de um cancelamento que perdeu a disputa | nenhum |
| rejeitada | A bolsa recusou a ordem; a reserva volta na hora | nenhum |

Quatro regras importam mais que os próprios estados:

* **Pedido de cancelamento não é cancelamento.** O jogador pede; o cancelamento só vale quando o lado da execução confirma. Entre os dois, execuções ainda podem acontecer.
* **Uma execução não pode ser desfeita.** Se 40 ações foram executadas antes de o cancelamento valer, essas 40 ações são do jogador. A recuperação para "parcialmente executada, depois cancelada" não é compensação. É registrar o que foi executado, cancelar o resto e liberar só a parte não usada da reserva.
* **Toda execução tem identidade.** Um id único de execução faz com que uma entrega duplicada não mude nada.
* **Toda transição verifica a versão que leu.** O mesmo padrão de update condicional que a Parte 3 usou para o poder de compra, aplicado à linha da ordem.

Depois de cada execução de teste, os mesmos invariantes precisam valer: poder de compra nunca negativo; reservado mais gasto mais disponível igual ao caixa inicial; quantidade executada mais restante mais cancelada igual à quantidade da ordem; posições iguais à soma das execuções registradas; e a verificação de reconciliação da Parte 3 continua passando.

A imagem abaixo segue a ordem do início do post em um eixo de tempo. Repare na segunda execução: ela acontece antes de o cancelamento valer, mas chega depois do pedido de cancelamento, e essa diferença é o problema inteiro.

![Linha do tempo de uma ordem: uma execução de 40, um pedido de cancelamento e uma execução de 25 que acontece antes de o cancelamento valer, mas chega depois do pedido](https://rso2zax703psuq0y.public.blob.vercel-storage.com/postscontent/1792540463260-04qae2bgkvl8.jpeg)

## O vocabulário, aplicado uma vez

Três termos bastam para este post, e cada um corresponde a uma escolha que o Bullpen teve de fazer.

**Compensação versus recuperação.** Uma compensação é uma nova transação que reverte o efeito de uma anterior, como liberar uma reserva. Alguns passos não têm compensação, como uma execução. Para esses, a saga precisa de um caminho de recuperação que segue para frente. Escrever qual é qual, passo a passo, é onde fica a maior parte do trabalho de design.

**Orquestração versus coreografia.** Numa saga orquestrada, um coordenador diz a cada passo o que fazer em seguida e é dono do estado. Numa saga coreografada, cada participante reage a eventos dos outros e ninguém é dono da visão completa. O Bullpen tem um dono das ordens e um banco, então a orquestração é o encaixe natural; a única pergunta aberta é onde o orquestrador mora.

**A matriz de sagas.** _Software Architecture: The Hard Parts_ descreve sagas em três dimensões: comunicação síncrona ou assíncrona, consistência atômica ou eventual, coordenação orquestrada ou coreografada. Isso dá oito combinações, e o livro dá nome a cada uma. A ordem do Bullpen cai em assíncrona, eventual e orquestrada, que o livro chama de Parallel Saga: as execuções chegam quando chegam, a ordem converge para um estado final em vez de fazer commit de tudo de uma vez, e um coordenador conduz. Para erros nesse formato, o livro aposta menos em cadeias de updates de compensação e mais em uma máquina de estados que sempre sabe onde cada transação está e a corrige com novas tentativas ou uma correção manual. Essa máquina de estados é onde a Decisão 1 começa.

## Decisão 1: onde a saga roda

**Condições.** O Bullpen roda no Vercel Hobby com um banco Neon. Quatro peças da plataforma poderiam conduzir ou cronometrar o ciclo de vida, e os limites delas moldaram as opções:

| Peça da plataforma | Limite relevante | Para onde empurra |
|---|---|---|
| Vercel Workflows | 50.000 eventos por mês no Hobby; um passo custa pelo menos 3 eventos; os payloads de uma execução encerrada ficam legíveis por 1 dia | Um orquestrador durável, se cada ordem tiver poucos passos |
| Vercel Queues | Entrega pelo menos uma vez, sem ordem garantida, sem dead-letter queue embutida | Consumidores idempotentes, com a ordem tratada nos dados |
| Cron jobs | Uma vez por dia, em qualquer momento dentro da hora | Não serve para nada que precise reagir em segundos |
| Vercel Daemons | Beta público desde 15 de outubro de 2026; só em produção; 10 Active CPU-horas por mês no Hobby | Um loop de polling que sobrevive a uma função |

As fontes são [preços e limites do Workflows](https://vercel.com/docs/workflows/pricing), [retenção do Workflows](https://vercel.com/docs/workflows/retention), [conceitos do Queues](https://vercel.com/docs/queues/concepts), [uso de cron jobs](https://vercel.com/docs/cron-jobs/usage-and-pricing) e [limites dos Daemons](https://vercel.com/docs/daemons/limits). Duas outras condições pesaram mais que qualquer número dessa tabela. O Postgres e um motor de workflow não conseguem fazer commit juntos, então qualquer coisa fora do banco precisa de uma passagem confiável. E no Hobby os payloads de uma execução de workflow encerrada ficam legíveis por um dia, então o Postgres precisa guardar sozinho tudo o que explica e recupera uma ordem.

**Opções.**

* **Uma máquina de estados no Postgres.** A linha da ordem é o estado; cada transição é um update condicional em uma transação; um sweep avança ou sinaliza ordens que pararam de andar.
* **Vercel Workflows como orquestrador.** Cada passo é uma função `'use step'` que roda uma transação em `trading`; o workflow guarda a sequência, as novas tentativas e as esperas. A Vercel documenta [uma receita de saga](https://workflow-sdk.dev/cookbook/common-patterns/saga) em que as compensações são escritas explicitamente e rodam em ordem inversa; não existe rollback embutido por passo.
* **Vercel Queues com coreografia.** Cada passo publica o próximo evento; os consumidores reagem. O estado continua no Postgres.

A máquina de estados precisa de um sweep, e o sweep precisa de um relógio que rode mais de uma vez por dia. Essa subescolha tem suas próprias opções: um [Vercel Daemon](https://vercel.com/docs/daemons) fazendo polling a cada poucos segundos, uma mensagem atrasada no Queues por ordem, usada como timer, ou um `sleep` de workflow se o Workflows vencer.

**Escolha.** A máquina de estados no Postgres, conduzida por um daemon. O daemon chama uma rota de sweep autenticada a cada 5 segundos enquanto houver alguma ordem aberta, e espaça para a cada 60 minutos quando não há nenhuma. Nesse ritmo, um dia sem ordens mantém o Neon acordado cerca de 2,3 horas, por volta de US$ 1,86 por mês; esse número é calculado, não lido no Neon. O sweep em si é só mais updates condicionais: cada passo verifica o estado da ordem antes de agir, então rodá-lo duas vezes, atrasado ou depois de uma queda não muda nada.

**Por quê.** Foi a opção que eu consegui construir e medir sem uma nova integração, e ela passou sozinha em todos os testes de correção abaixo. Um daemon parado só atrasa a recuperação: um teste o deixa parado por dias, e o sweep seguinte termina todas as ordens com os invariantes intactos. O custo dele é pequeno. Localmente, uma chamada de sweep leva cerca de 2,1 ms de CPU do daemon; a cada 5 segundos durante o mês inteiro, são 518.400 chamadas e cerca de 0,31 das 10 Active CPU-horas da conta Hobby.

Não construí o Workflows e não o medi. O único ganho que um `sleep` de workflow poderia mostrar aqui é o custo ocioso do Neon, porque um timer que só dispara quando uma ordem precisa deixa o banco dormir. Medir isso exige um deploy com o Workflows escrevendo no Neon. Também traria a passagem descrita abaixo, que é um risco novo para uma base que já passava nos testes. Então a conclusão é estreita: a Parte 4 não precisou do Workflows. Não é que o Workflows seria pior.

A passagem é a parte que eu subestimei no papel. Se o Workflows conduzisse a ordem, a linha da ordem faria commit no Postgres e o workflow começaria na Vercel, e esses são dois commits separados. Uma ordem poderia fazer commit e nunca começar, ou uma nova tentativa poderia começá-la duas vezes. A solução é um outbox: o evento de início é escrito numa tabela na mesma transação da ordem, e um despachante o envia e o marca como enviado, de forma idempotente. Escrevi sobre por que um outbox é um ledger e não uma fila nas [minhas notas sobre o transactional outbox](https://tiarebalbi.com/en/blog/the-transactional-outbox-is-not-a-queue). Uma máquina de estados pura no Postgres não tem passagem nenhuma, e isso acabou pesando tanto quanto qualquer número.

**O que me faria voltar atrás.** Custo, não correção. Um daemon que faz polling impede o Neon de escalar a zero. Se o sweeper mantiver o compute acordado mais de cerca de 8 horas por dia em média, o que dá cerca de 61 CU-horas e US$ 6,50 por mês a 0,25 CU no plano Launch do Neon, uma mensagem atrasada ou um `sleep` que só toca o Postgres quando o timer de uma ordem dispara passa a ser o condutor mais barato. Um segundo consumidor de eventos de ordem, ou um passo que precise esperar dias, também me faria voltar atrás.

## Decisão 2: o cancelamento que disputa com uma execução

**Condições.** As execuções chegam pelo menos uma vez e não necessariamente em ordem. Um cancelamento vale do lado da execução, não quando o jogador toca no botão. O ledger precisa estar certo depois de qualquer intercalação, não só das prováveis.

**Opções.**

* **O cancelamento vence.** Qualquer execução que chegue depois de "cancelamento solicitado" é rejeitada.
* **A última escrita vence.** O que chegar por último ao banco decide o estado final.
* **O horário da execução decide.** Uma execução que aconteceu antes de o cancelamento valer é registrada, chegue quando chegar; o cancelamento então se aplica só ao que restou.

**Escolha.** O horário da execução decide. Rejeitar uma execução real deixaria a posição do jogador fora de sincronia com o que o lado da execução fez, e a última escrita vencer faz a correção depender do tempo de rede.

**Por quê.** É a única opção em que os invariantes podem valer em qualquer intercalação, e ela reaproveita o mecanismo da Parte 3 em vez de criar outro. Cada transição é um update condicional na versão da ordem; cada execução é inserida com um id único, então uma segunda entrega da mesma execução encontra a linha já lá e não muda nada. Tratei a deduplicação como mais do que uma chave, pelos motivos das [minhas notas sobre idempotência como protocolo](https://tiarebalbi.com/en/blog/idempotency-is-a-protocol-not-a-key): a duplicata também precisa chegar enquanto a primeira entrega ainda está rodando.

As regras de ordenação do simulador estão no mesmo ADR. Toda execução tem um id e um horário na bolsa, e chega pelo menos uma vez, em qualquer ordem. A bolsa confirma um cancelamento com um horário efetivo, e toda execução que aconteceu antes desse horário é devida, chegue quando chegar. A reserva não usada só é liberada depois do horário efetivo mais uma janela de 30 segundos para execuções atrasadas, e o sweep tenta a liberação de novo até conseguir.

A disputa rodou de dois jeitos, contra um Postgres local. Uma execução exaustiva testou 8.346 ordens de entrega distintas de execuções, duplicatas, pedidos e confirmações de cancelamento. Uma execução aleatória testou 1.500 seeds com eventos em voo ao mesmo tempo, injetando 1.985 quedas e 915 entregas duplicadas. As duas tiveram zero falhas de invariante. Para conferir se o teste conseguia falhar, mudei uma linha para o ciclo de vida descartar qualquer execução que chegasse depois de um cancelamento: 4.330 das 8.346 execuções exaustivas falharam, e desfiz a mudança.

O que essas execuções não cobrem: uma conta e uma ordem pequena por execução, no máximo quatro execuções, um atraso maior que a janela de 30 segundos, muitas contas disputando ao mesmo tempo, uma bolsa real ou o próprio Neon. E uma coisa que elas ainda não explicaram: em 26 de outubro, uma execução desse teste na CI parou num erro do Postgres, `new multixact has more than one updating member`. Nenhuma verificação de conta falhou e a nova tentativa passou, mas ainda não encontrei a causa. Ela entra na lista do teste de carga da Parte 5.

**O que me faria voltar atrás.** Uma bolsa que reporte cancelamentos e execuções sem um horário de execução compartilhado. Aí "aconteceu antes de o cancelamento valer" não pode ser decidido, e a ordem precisaria do relatório final da própria bolsa antes de liquidar.

## Decisão 3: de onde vêm as execuções

**Condições.** A Parte 4 precisa de execuções que cheguem atrasadas, em pedaços e de vez em quando duplicadas, sob demanda, em testes que possam ser repetidos. O [ambiente de paper trading da Alpaca](https://docs.alpaca.markets/docs/paper-trading) documenta execuções parciais de tamanho aleatório em cerca de 10% das vezes em que uma ordem pode ser executada, o que a Parte 1 já usou como régua de realismo.

**Opções.**

* Chamar a API de paper trading da Alpaca.
* Escrever um simulador baseado no comportamento documentado da Alpaca.

**Escolha.** Um simulador determinístico atrás de um adaptador. Ele tem seed, consegue injetar um atraso ou uma queda numa fronteira nomeada e consegue forçar exatamente a disputa do início do post. É baseado no comportamento documentado da Alpaca, não uma cópia dele; o adaptador deixa espaço para rodar com a Alpaca de verdade depois.

**Por quê.** Testes de correção precisam reproduzir uma falha, e um sandbox externo não garante a mesma intercalação duas vezes. Adicionar um provedor também colocaria uma dependência nova justo na parte do Bullpen que precisa ser comprovadamente correta.

**O que me faria voltar atrás.** Uma pergunta que só a bolsa real responde, como a ordem em que ela confirma cancelamentos em relação às execuções. Isso é um experimento para a Parte 5 ou depois.

## O que eu quebrei de propósito

Cada falha abaixo tem um teste no repositório e uma entrada no relatório da Parte 4. Aqui o caminho feliz prova pouco; cada uma dessas precisa deixar a conta correta.

| Falha injetada | O que precisa valer | Resultado |
|---|---|---|
| Uma execução duplicada cuja primeira entrega fez commit, mas cuja confirmação se perdeu | Ledger, posição e reserva sem mudança pela segunda entrega | Valeu: a conta fica igual a um controle que viu a execução uma vez, mesmo quando toda execução é entregue duas vezes |
| Uma queda depois da escrita de um passo, mas antes de o sucesso ser registrado | A nova tentativa não duplica a reserva nem um lançamento no ledger | Valeu: cada passo foi parado em cada fronteira nomeada, antes e depois do commit, e rodado de novo; uma de cada, sempre |
| Uma compensação (liberar a reserva) que continua falhando | A ordem fica visivelmente travada, sinalizada, é tentada de novo e se recupera sem corromper a conta | Valeu: falhou em cinco sweeps, foi sinalizada na terceira e liberada por inteiro no primeiro sucesso |
| O sweeper parado no meio do ciclo de vida | A ordem ainda chega a um estado final | Valeu: depois de dias parado, o sweep seguinte terminou todas as ordens; um redeploy não foi testado à parte |

Um rollback para um deploy anterior e a exclusão de um deploy são falhas diferentes de uma queda. Os [conceitos do Workflows](https://vercel.com/docs/workflows/concepts) dizem que as execuções ficam presas ao deploy em que começaram, e execuções presas a um deploy excluído nunca terminam sozinhas. Não construí o Workflows, então essas duas ficam como leitura, não como teste.

A liberação que falha é a que eu mostraria primeiro. O design da Cloudflare, do [post de junho sobre rollbacks no Workflows](https://blog.cloudflare.com/rollbacks-for-workflows/), para os rollbacks restantes e marca a execução como erro quando um rollback falha. O Bullpen não pode parar: o dinheiro continua reservado, e o jogador precisa dele de volta. Então a ordem continua cancelada, o valor não usado continua contado como reservado, o que mantém todos os invariantes válidos, e o sweep tenta de novo a cada passada. Na terceira falha, a ordem é sinalizada. O sweep seguinte que deu certo liberou os 650 e limpou o sinal. O que falta é o alerta: hoje o sinal é uma coluna, e ninguém é avisado.

## O que a produção encontrou e os testes não

Todos os testes acima passaram antes da primeira ordem real. Mesmo assim, a produção encontrou três problemas nos dois primeiros dias, e os três tinham o mesmo formato: o teste fornecia algo que a produção nunca forneceria.

* **Login.** Os testes assinavam o próprio token no formato que meu código esperava. O SDK do Neon guarda a sessão num cookie assinado com um segredo, em outro formato, então toda requisição autenticada voltava 401. A correção pega o jogador da própria verificação de sessão do SDK (ADR-0022), e o teste agora usa o cookie real do SDK.
* **Migrations.** Fazer o deploy do código e aplicar a migration eram dois passos manuais. Duas vezes no mesmo dia, a produção rodou código cujas tabelas ainda não existiam. Agora um build de produção aplica as migrations antes de fazer o build, e falha se não conseguir (ADR-0023).
* **Execuções.** A bolsa simulada só executava a um preço com menos de 5 minutos, e todo teste de ordem criava um preço de poucos segundos. Os preços de ações no Bullpen são de fechamento, com horas de idade quando alguém opera. Minhas três primeiras ordens reais, 90 AMZN, 12 V e 3 AAPL, esperaram 15 minutos e foram canceladas pelo sweep. O ledger estava certo: cada reserva voltou por inteiro. A regra é que estava errada para os dados. Agora cada preço diz se é ao vivo ou de fechamento, e só um preço ao vivo tem o limite de 5 minutos (ADR-0024). Depois que a correção entrou no ar, em 26 de outubro, uma compra a mercado de 1 AMZN foi de reservada a executada no mesmo segundo, a US$ 248,00, o fechamento de 23 de outubro, e os US$ 12,40 não usados da reserva voltaram.

O último é o argumento deste post aparecendo sem ser chamado. A regra era sobre tempo, e o relógio do teste não era o da produção.

## As decisões menores

| Decisão | Escolha | Evidência |
|---|---|---|
| Login | Auth gerenciado do Neon, só GitHub; um jogador aparece como "Player" mais quatro caracteres, nunca como nome de usuário do GitHub | Logins reais em bullpen-app.tiarebalbi.com desde 21 de outubro; uma conta só sob primeiros logins concorrentes nos testes |
| Status da ordem ao vivo | Polling a cada 3 s enquanto há ordem aberta, a cada 15 s nos outros casos, pausado em aba escondida | 153 bytes com gzip por poll sem ordens, 562 com sete, medido localmente |
| Testes de contrato | Entre o app de trading e o código do ciclo de vida | Testes de contrato mais os testes das próprias rotas de ordem; o contrato pegou dinheiro enviado com 16 casas decimais onde ele permite 8 |
| Compute para toda medição | Neon fixo em 0,25 CU | `SHOW max_connections` retornou 112 em 20 de outubro de 2026; a tabela de tamanhos de compute do Neon lista 104 para esse tamanho |

Essa última linha é a lição da Parte 3 aplicada desde o começo: a configuração é lida no sistema antes de qualquer número entrar no post.

## O que a construção mudou nos registros

Quatro registros de decisão sustentam o design. O ADR-0017 é o ciclo de vida da ordem como máquina de estados no Postgres, e o ADR-0018 é o sweeper, com o ADR-0021 definindo a espera ociosa dele em 60 minutos. O ADR-0019 faz o leaderboard contar o poder de compra reservado como caixa: sem ele, o patrimônio de um jogador caía pelo valor reservado enquanto uma ordem estava aberta, o que uma verificação de patrimônio depois de cada teste de ordem agora pega. O ADR-0020 é o login pelo auth gerenciado do Neon. A produção acrescentou mais três, um para cada problema acima (ADR-0022 a ADR-0024), e o ADR-0025 acrescenta o fechamento anterior ao contrato de preços para a tela de trading.

Os registros da Parte 3 ficaram como Proposed até o post sair. Desta vez aceitei os primeiros durante a construção, porque o ADR-0016 diz que só uma decisão Accepted pode nomear um nó novo no modelo, e o sweeper e o Neon Auth precisavam aparecer no momento da Parte 4. Os ADR-0023 a ADR-0025 foram aceitos com este post.

O momento da Parte 4 no modelo de arquitetura agora é o atual, e o explorador na [página de arquitetura](https://bullpen.tiarebalbi.com/architecture) o segue. O nó Order Saga que a primeira semana previu aparece como previsto e não construído: não existe serviço de saga, só passos dentro do schema `trading` e um daemon que os mantém andando. A tag `predictions-week-1` não se moveu.

O app de trading está aberto para ordens em [bullpen-app.tiarebalbi.com](https://bullpen-app.tiarebalbi.com). Todo mundo começa com 100.000 em dinheiro de mentira. Um jogador logado vê seu patrimônio e sua posição no ranking no cabeçalho, uma watchlist com o que tem, uma tabela de posições com custo médio e lucro ou prejuízo, um ticket de compra a mercado e uma linha do tempo, passo a passo, da ordem aberta, com um botão de cancelar. "Cancelamento solicitado" tem palavras e marca próprias e nunca diz que o dinheiro voltou; uma ordem cancelada mostra o que foi executado e o que foi liberado. Ainda não há venda, ordem limitada nem gráfico, porque nenhum deles tem dados por trás. Eles chegam com os preços ao vivo na Parte 5.

## Quanto a Parte 4 custou

Medi o custo por ordem localmente, em 40 compras a mercado:

| Por ordem | Medido localmente |
|---|---|
| Comandos SQL, de enviar a ordem até ela ser executada | 39,8 |
| Linhas escritas | 17,6 |
| Invocações de função (a ordem mais os polls depois dela) | 1,23 |
| CU-horas e transferência no Neon | pendente |

O daemon usou cerca de 37 segundos de CPU por dia no ritmo de 5 segundos, localmente; isso não é uma leitura de Active CPU da Vercel. A produção roda desde 21 de outubro, tempo curto demais para uma leitura semanal antes deste post. O Active CPU do daemon, as CU-horas do Neon desde que as ordens existem, o custo do Neon Auth neste plano e o uso da Vercel entram no registro de custos quando a semana fechar, e a Parte 5 os apresenta. Até lá, eles ficam como pendentes aqui, não estimados.

## De onde vem o tempo de uma transação

Divida o trabalho onde o tempo obriga, e mantenha cada passo atômico. Defina a compensação, ou o caminho de recuperação quando uma ação não pode ser desfeita, antes de escrever o passo.

A saga no Bullpen veio do relógio, não do organograma nem do diagrama de deploy. Antes de usar uma saga no seu próprio sistema, vale perguntar qual dos dois é o seu caso: o trabalho é longo porque espera o mundo, ou só porque cruza uma fronteira que você desenhou?
