How Clojure Freed Me from the Ceremony
A year into working at Nubank — where the backend runs almost entirely on Clojure — I’ve had enough time to form a real opinion rather than a first impression. The thing that keeps standing out to me is how much less code I write to accomplish the same things I used to do in C# or Java. Not fewer features, not less correctness — fewer words. When I was watching Rich Hickey talk about how standard libraries in some OOP languages end up with deeply layered class hierarchies just to represent simple concepts, something clicked. The complexity isn’t always in the problem. A lot of the time it’s in the language’s way of expressing the solution.
In object-oriented languages I’ve worked with, solving a backend problem comes with a ritual. You need an interface so you can test it. You need a class to implement it. You need a constructor, probably a factory, getters and setters for the fields, and somewhere in there a design pattern that wraps the whole thing in just the right level of abstraction so it doesn’t feel hacky. None of this is wrong — it’s the grammar of the language, and experienced engineers use it well. But it is ceremony. A significant chunk of the code you write isn’t the logic of the problem; it’s the scaffolding the language requires to hold the logic. In Clojure, most of that scaffolding disappears. You write a function that takes data and returns data. If the function gets complicated, you break it into smaller ones. If you need polymorphism, multimethods let you dispatch on anything — not just type. If you need to manage stateful components like a database connection or a message queue, Stuart Sierra’s component pattern handles lifecycle cleanly without turning your codebase into an inheritance diagram.
To be fair, none of this means OOP is overengineered or that people who use it are doing it wrong. The boilerplate exists for good reasons — encapsulation, extensibility, the ability to reason about boundaries. Languages like C# and Java have gotten genuinely good at managing complexity at scale. The difference is that in Clojure, the higher-level abstractions are already there by default. Data is immutable. Functions are first-class. The language nudges you toward simplicity rather than requiring discipline to achieve it. What used to take an interface, two implementations, and a factory now takes a map and a function. The core logic — the part that actually solves the problem — is exposed rather than buried.
After a year of this, going back to a class-heavy codebase feels a bit like snowboarding — you spend a good chunk of the morning renting boots, adjusting bindings, layering up, and getting to the top of the mountain before you actually get to ride down. And riding down is great, genuinely worth it. Just a little bit slower before the fun begins. I don’t think Clojure is the right tool for everything, and I still respect what well-crafted OOP looks like. But for backend systems where the work is fundamentally about transforming data — taking something in, doing something to it, producing something out — functional programming with Clojure maps almost perfectly to the shape of the problem. The code ends up looking a lot like the logic, and that’s a relief.
Software Theory