kadence
Decisions

ADR-003 — No runtime dependencies in the core

zod costs 23.6 ms to import — 15% of the startup budget to validate a seven-field object.

Date: 2026-09-02 · Status: accepted · Scope: stack

Context

After ADR-001, 160 ms is left for everything the CLI does. The cost of merely importing each candidate was measured (best of 3, Node v25.8.2, unbundled):

PackageImportSize
zod 4.5.423.6 ms5.8 MB
commander 15.0.017.1 ms207 KB
yaml 2.9.013.5 ms686 KB
gray-matter 4.0.313.0 ms39 KB
ulidx 2.4.111.5 ms66 KB
ulid 3.0.210.1 ms69 KB
citty 0.2.28.3 ms35 KB
picocolors 1.1.17.7 ms6 KB
valibot 1.4.25.0 ms1.8 MB
cac 7.0.03.3 ms41 KB

zod alone costs 15% of the entire budget — to validate an object with seven fields.

So the alternative was measured too:

Hand-rolledImport costThroughputCorrectness
ULID (26 chars, Crockford base32)0 ms100k in 132 mssorts by time; 50k unique, no collisions
Event validation (7 fields, type guards)0 ms100k in 17.5 mscatches all 6 classes of error

Both are about 30 lines.

Decision

The core has no runtime dependency at all. ULID and event validation are hand-rolled and under test.

This is not "not invented here". The argument is measurable and singular: general validation libraries exist for schemas described at runtime that change. Our event shape is fixed in types at compile time, and the check is needed at exactly one boundary — reading a file a person or a merge could have corrupted. For that, a 5.8 MB engine gives nothing a 20-line function does not, and costs a fifth of the startup budget.

ULID was chosen over UUIDv4 for a separate reason: the first 10 characters are a timestamp in Crockford base32, so sorting ids lexicographically matches sorting them chronologically. Event order becomes a property of the id rather than of how far apart machine clocks have drifted.

Consequences

  • About 34 ms of budget saved; npm audit has nothing to report on the core; the supply chain has zero links.
  • ~60 lines of cryptographically sensitive (ULID uses node:crypto) and validation code that we must test ourselves — with property tests, not just examples.
  • What would make us revisit it: if the event schema becomes extensible by plugins with their own fields, fixed validation stops working, and valibot becomes the cheapest engine to fall back to.

On this page