Docs/Architecturev0.0.1-preview
Architecture overview
The write path, the four network planes, and where TulaDB sits in a payments stack — a reference architecture.
TulaDB is a single-purpose engine: a native daemon (tuladbd, written in C17) that owns its own write-ahead log, snapshot recovery, Raft-style replication, and binary wire protocol. No embedded general-purpose database, no external coordination service — the correctness argument has one moving part.
The write path
Every accepted write travels the same short path:
client → binary TCP listener → validate + idempotency check
→ stage on sharded account state (undone if the write fails)
→ WAL append (fdatasync / io_uring) ← durable HERE
→ quorum acknowledgement (clustered)
→ acknowledge client
→ project to query indexes
Two properties fall out of the ordering. First, acknowledged means durable: the acknowledgement is gated on WAL durability (and, in a cluster, on a quorum of nodes holding the entry) — not on an application-level buffer. Second, applied means ordered: transfers apply under a single committed order, coordinates (batch_seq, batch_index), which is the same order every replica applies, every recovery replays, and every history query pages through. Timestamps come from a hybrid logical clock, so ordering stays consistent across nodes even when wall clocks aren’t.
There is one write path. Every client write on the wire is a batch — a single transfer is a batch of one — so a single call and a batched call can never disagree about validation order, idempotency, or the recorded outcome. The SDKs keep their single-operation calls; underneath, they send a one-item batch. When a request breaks more than one rule at once, the engine reports the consistency reason (wrong ledger, currency mismatch, control-account rule) ahead of account eligibility (closed, inactive, insufficient funds), on every path.
Accounts are named by (ledger_id, currency, id). Every account-addressed request carries that full key, with a 128-bit id; a key no account carries answers ERR_NOT_FOUND. In this release an id is still unique across the whole cluster — creating the same id under a second ledger or currency answers ERR_ACCOUNT_CONFLICT (44).
Throughput comes from batching, not from weakening the contract: the engine groups writes into batches, amortises one fdatasync across a group, and fans application out across a partitioned account directory — while each account’s mutations stay strictly sequenced.
Four network planes
Each node separates traffic onto dedicated listeners, so one plane’s load can never starve another’s:
| Plane | Default port | Carries |
|---|---|---|
| Client | 7700 |
Application reads and writes |
| Query | 7800 |
Read-only queries and the analytics export cursor — write-shaped messages refused |
| Fabric | 8700 |
Raft replication data: log entries, snapshot install, a joining node’s history sync — peer-authenticated (mTLS required off loopback) |
| Heartbeat | 9700 |
Consensus control: heartbeats, elections |
The separation is deliberate correctness engineering: a multi-megabyte snapshot transfer on the fabric plane cannot delay a heartbeat, so bulk recovery traffic can’t destabilize leadership.
Reference architecture
A production deployment has three tiers around the engine cluster:
your services (payments API, workers, settlement jobs)
│ SDKs (mTLS) │ REST (API key)
▼ ▼
┌────────────────┐ ┌──────────────────┐
│ direct SDK │ │ SDK gateway │ stateless, scale out
└───────┬────────┘ └────────┬─────────┘
▼ writes → leader▼
┌──────────────────────────────────────────────────────┐
│ TulaDB cluster — 3 (or 5) voters, one per AZ │
│ Raft replication · quorum-gated durability │
│ + optional learners for read scale / DR seeding │
└──────┬──────────────────────┬────────────────────────┘
│ query port (7800) │ metrics sidecar (9145, loopback)
▼ ▼
analytics export Prometheus / Grafana / dashboard
→ Iceberg lakehouse
- Write tier. Services submit through a leader-aware SDK or the REST gateway. Followers proxy writes to the current leader, so clients don’t need perfect routing to stay correct.
- Engine tier. Three voters across availability zones is the baseline: it survives one zone loss with no data loss and no failover scripts. Learners replicate the log without affecting quorum — use them for read fan-out or warm standbys.
- Read tier. Operational reads hit the query ports; the dashboard and support tools live here. Warehouse analytics leave the cluster entirely through the export pipeline, which reads only committed state from the query port; the dashboard can then query the lake directly.
Where the boundary sits
TulaDB owns deterministic financial state: balances, holds, postings, rejected outcomes, idempotency, ordering, durability, recovery. Everything with business judgment in it — fraud scoring, routing, orchestration, compliance workflow, message-format parsing — stays in your services above the ledger. The engine’s job is to make the state layer boring: explicit about what is durable and when, and predictable under crash and partition.
Going deeper
- Durability and recovery — the WAL, snapshots, backup, and point-in-time restore.
- Replication and consistency — Raft, elections, membership, and read guarantees.
- Joining a node — how a new member becomes an exact full copy before it can vote.
- How we verify correctness — invariants and Jepsen-style fault injection.