Two years. Long enough that the novelty is gone and what’s left is the actual experience of living with the decision. Here’s the retrospective I’d want to have read before starting.

What Held Up

The functional model for batch computation. Risk calculations, data transformations, report generation — pure functions over immutable data. This was a genuine fit. The code is testable, composable, and parallelisable in ways that the equivalent Java would fight against.

At one point we needed to parallelise a report generation job that had previously been sequential. In Java, this would have required careful analysis of shared state, locks, thread safety. In Clojure, the computation was already pure — we wrapped the outer loop in pmap and got 6x throughput improvement in an afternoon.

The REPL for exploratory debugging. Production incidents involving financial calculations often require interrogating the data to understand what happened. Being able to load production data into a REPL, reconstruct the state at the time of the incident, and interactively run calculations against it was genuinely better than the equivalent Java workflow (reproduce in a test, add logging, redeploy, trigger again).

Data as the interface. Clojure’s pervasive use of maps and sequences as data types — rather than domain-specific objects — made integration between components straightforward. You pass maps around; any function that knows the map shape can work with it. No interface proliferation, no adapter layers.

What Didn’t Hold Up

Dynamic typing at scale. In a team of three, everyone knows the shape of the data. At six people with high turnover, the lack of a type system was a maintenance burden. We added clojure.spec definitions for critical data shapes and wrote generative tests against them. This helped, but it was compensating for an absence rather than using a feature.

The failure mode was subtle: a field renamed in one part of the system would silently be ignored in another because Clojure’s keyword lookup returns nil for missing keys rather than throwing. These bugs were found in testing, but they required vigilant discipline that the Java compiler would have caught for free.

Hiring and onboarding. Every engineer who joined the team needed to learn Clojure from scratch. We’d budgeted two weeks for this; the realistic number was six weeks before someone was confidently writing idiomatic code and twelve weeks before they were comfortable with the more unusual parts (macros, transducers, core.async).

For a team that’s mostly stable, this is manageable. For a team with turnover, it’s a significant hidden cost.

Tooling gaps at the edges. The core tooling is excellent (Cursive, CIDER, leiningen/tools.deps). The surrounding ecosystem is patchy. We needed APM integration (worked fine via Java interop), custom serialisation for a binary protocol (required writing Java interop code, which is valid but breaks the Clojure-only mental model), and some debugging workflows that are second-nature in Java required creative workarounds.

Stack traces. Clojure stack traces are infamous, and the reputation is earned. An exception from inside a transducer pipeline or a multi-method dispatch produces a trace full of Clojure runtime internals before you get to your code. You get used to reading them, but “you get used to it” is a weak endorsement.

The Things Nobody Tells You

Clojure’s immutable persistent data structures have a performance profile that surprised me: they’re fast for read-heavy workloads and for functional update (structural sharing means small modifications are cheap), but slower than mutable Java collections for bulk writes. Our initial risk calculation loaded positions into a Clojure map and then updated it thousands of times — this was significantly slower than the equivalent mutable HashMap until we restructured to build the map once rather than incrementally updating it.

Also: the community is excellent but small. When you hit a problem that isn’t in the top-10 Stack Overflow answers, you’re filing GitHub issues or reading source code. This is fine for engineers who like going deep, and friction for engineers who are used to finding an answer in five minutes.

What I’d Carry Forward

The functional thinking is transferable. When I moved to Go, I wrote far better code because of the Clojure years — not Go code that looks like Clojure, but code that thinks carefully about data flow, minimises shared state, and prefers data transformations over mutation. That’s a Clojure gift.

The specific choice of Clojure for a new project: I’d want a team that’s bought in and a problem that’s a genuine functional fit. It’s not a general-purpose choice for all financial software. For batch computation and data pipelines it’s excellent. For latency-sensitive trading systems with complex state: probably not.