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.

On this page
  1. The write path
  2. Four network planes
  3. Reference architecture
  4. Where the boundary sits
  5. Going deeper

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