Ledger de dupla entrada (contabilidade) em Clojure sobre XTDB — um banco de dados imutável e bitemporal, no espírito do que o Nubank faz com Datomic. Cada lançamento é append-only: nada é atualizado ou apagado, só acrescentado. O saldo é sempre derivado do histórico.
A invariante central da dupla entrada: toda transação soma zero — o que sai de uma conta entra em outra(s). O domínio garante isso antes de persistir.
- Dupla entrada de verdade — transações precisam balancear (soma = 0); o saldo é a soma dos lançamentos, nunca um campo mutável.
- XTDB (imutável) — persistência append-only; roda com um node em memória, sem serviço externo.
- Arquitetura hexagonal — a persistência é um
defprotocol(port). Há dois adapters: em memória e XTDB. O domínio é Clojure puro, sem I/O. - 100% de cobertura de linhas — domínio, serviço, ambos os adapters (o de XTDB testado contra um node real em memória) e a camada HTTP.
- OpenAPI/Swagger — documentação interativa em
/docs.
- Linguagem: Clojure 1.12 (na JVM 21)
- Banco: XTDB 1.x (node em memória)
- HTTP: handler ring puro (testável sem servidor) + Jetty no bootstrap
- JSON: Cheshire
- Testes/cobertura: clojure.test + Cloverage
src/ledger/
├── domain.clj # regras puras (validação, saldo)
├── ports.clj # protocolo LedgerStore
├── service.clj # casos de uso
├── adapters/
│ ├── memory.clj # store em memória (atom)
│ └── xtdb.clj # store XTDB (append-only)
├── http.clj # handler ring (rotas + erros)
├── openapi.clj # spec OpenAPI + Swagger UI
└── main.clj # bootstrap (XTDB + Jetty)
Requer o Clojure CLI (ou o deps.clj) e a JVM 21.
clojure -M:run # http://localhost:3000docker compose up --build| Método | Rota | Descrição |
|---|---|---|
| POST | /accounts |
Cria uma conta (id?, name) |
| GET | /accounts/{id} |
Consulta uma conta |
| GET | /accounts/{id}/balance |
Saldo (em centavos) |
| GET | /accounts/{id}/statement |
Extrato (lançamentos) |
| POST | /transactions |
Posta uma transação (deve balancear) |
| GET | /docs |
Swagger UI |
| GET | /health |
Health check |
Exemplo (transferência de R$ 10,00 da Alice para o Bob):
curl -X POST localhost:3000/accounts -d '{"id":"alice","name":"Alice"}'
curl -X POST localhost:3000/accounts -d '{"id":"bob","name":"Bob"}'
curl -X POST localhost:3000/transactions \
-d '{"entries":[{"account-id":"alice","amount":-1000},
{"account-id":"bob","amount":1000}]}'
curl localhost:3000/accounts/alice/balance # -> {"account-id":"alice","balance":-1000}clojure -M:coverageA cobertura de linhas é 100% em todos os namespaces (o main/bootstrap fica
de fora). O Cloverage também reporta cobertura de forms (métrica mais rígida,
que instrumenta a expansão de macros como cond/try); ela fica em ~99% e é
usada apenas como piso no gate.
MIT.