Before clojure.spec, our FIX message parser had a test suite with 40 hand-written test cases. We’d been running it in production for 18 months without incident.
After we added spec and ran the property-based tests overnight, it found 7 edge cases we hadn’t written tests for — including one where a negative zero value (-0.0) in a price field caused the downstream risk calculation to produce NaN, which propagated silently through the pipeline and ended up in the regulatory report as a blank field.
That was the end of hand-written validation tests for external data.
What clojure.spec Provides
clojure.spec is a library for describing the structure of data and functions. It provides:
- Validation: is this data valid according to this spec?
- Conformation: parse and transform data according to a spec
- Generation: generate random valid examples of data matching a spec
- Instrumentation: automatically check function arguments and return values against specs
For financial data validation, the first three are immediately useful.
Defining Specs for Trade Data
| |
Now validation is declarative:
| |
s/explain shows exactly which predicate failed:
| |
This is the output you show to the operations team when an upstream system sends bad data.
Generative Testing: Finding the Unknown Unknowns
The real value of spec is generative testing. Specs don’t just validate — they can generate random valid examples. Libraries like test.check use these generators to run hundreds of random inputs through your code looking for failures.
| |
This generates 1000 random trades (random symbols, random prices within spec bounds, random quantities) and asserts that the output is always a valid processed trade with finite P&L.
The generator can be customised to produce more realistic distributions:
| |
Spec at the System Boundary
The most valuable use of spec is at the boundary between external and internal systems:
| |
Every message that enters the system has been validated against the spec before any processing occurs. Invalid messages are rejected with useful error information; they never reach the core logic.
This is the shift spec enables: instead of defensive programming scattered throughout the codebase (if (price < 0) throw...), you define correctness once in the spec and enforce it at the boundary. The interior code can assume it’s receiving valid data.
Spec for Function Contracts
s/fdef defines specs for function arguments and return values:
| |
With stest/instrument, calls to compute-pnl are automatically validated against the spec at runtime — useful in development and testing:
| |
In production, instrumentation is typically disabled (it adds overhead). You rely on boundary validation having caught invalid data before it reaches instrumented functions.
The Bugs Spec Found
The generative tests found seven issues in our FIX parser within the first overnight run:
- Negative zero price (
-0.0) — now caught bypos?predicate - Symbol with lowercase (
EUR/usd) — parser didn’t normalise case consistently - Timestamp in Unix epoch 0 — a missing timestamp field defaulted to
1970-01-01 - Quantity exactly at Int.MAX_VALUE — a conversion bug from long to int
- Empty string venue — the venue field was optional but we used it without null-checking
- Price exactly 0.0 — not a FIX parsing bug but exposed a division-by-zero in a ratio calculation
- Side field ‘B’ instead of ‘BUY’ — an older FIX version used a different format we hadn’t handled
Six of these seven bugs existed in code that had been in production for 18 months. They were triggered by input combinations that never occurred naturally but are entirely plausible in production (especially #7 — a vendor sends older FIX format during testing).
Spec’s generative testing is not a replacement for domain knowledge and hand-written edge cases. It’s an amplifier — it exercises the boundaries of your spec’s solution space automatically and finds cases you didn’t think to test for. For financial data processing, that amplification has real value.