H ho͝osnul

From Clojure to Java : Rethinking Ziggurat Wrapper

A couple of months ago, I wrote about the story behind Borobudur, a thin Java wrapper around Ziggurat, GoPay’s Kafka stream processing framework written in Clojure. The motivation behind the project was simple: I wanted Java developers to be able to build Kafka consumers without having to write Clojure. Since Ziggurat already provided all the production-ready infrastructure we needed, wrapping it from Java seemed like the most practical solution.

The project turned out better than I had expected. Borobudur eventually made its way into production, and several Kafka consumers at GoPay are now built on top of it. It solved the problem I originally set out to solve, and I was quite happy with the result.

This week, however, I had a chance to revisit one of those production workers. My task had nothing to do with Borobudur itself. I simply needed to migrate its metrics from StatsD to Prometheus. I expected it to be a routine maintenance task, but somewhere along the way I encountered an idea that completely changed how I think about Java and Clojure interoperability.

The Problem

I honestly cannot remember whether the idea came from an article I was reading or from an AI assistant during one of our conversations. The exact source no longer matters. What caught my attention was a simple statement that Java code can be called directly from Clojure.

I had been fixated on the idea that we could only use Java libraries after publishing them to Maven or an internal artifact repository. I was also too focused on figuring out how to run Clojure from Java, rather than considering the opposite direction.

Once I started thinking about the problem differently, a new idea emerged almost immediately. Instead of wrapping Ziggurat so that Java could host it, why not let Ziggurat remain exactly as it was designed, as a Clojure application, and move only the business logic into Java? Since Clojure has excellent interoperability with Java, the framework could simply instantiate and call Java classes whenever it needed to process Kafka messages.

The idea sounded almost too simple, so I decided to build a quick proof of concept. I wrote a Java class that implemented a minimal Kafka handler whose only responsibility was printing the incoming message. The Ziggurat application remained entirely in Clojure, and the handler was instantiated and invoked from there. To my surprise, everything worked on the first attempt. There was no complicated interoperability layer, no reflection-heavy wrapper, and no special tricks. From Ziggurat’s perspective, it was simply calling another JVM class.

Unfortunately, the proof of concept represented only the easiest part of the problem. Real production workers are significantly more complicated than printing Kafka messages. They depend on generated Protobuf classes, internal service clients, shared utility libraries, configuration modules, and various third-party dependencies. While the business logic itself could now live in Java, I still had to maintain a Clojure project using Leiningen, and that was where the developer experience started to break down.

Neither I nor many of my teammates are particularly familiar with Leiningen or the conventions commonly used in Clojure projects. Every time we needed to modify the project structure or adjust dependencies, we had to spend extra time remembering how everything fit together. On top of that, I have always found Leiningen relatively slow during development, especially compared to the Gradle workflow that most Java developers are already comfortable with.

A Different Perspective

I described these frustrations to an AI assistant while exploring possible alternatives. Its suggestion surprised me more than the interoperability discovery itself. Instead of assuming that Leiningen was mandatory, it suggested using Gradle to build both the Java and Clojure code within the same project.

That immediately made me pause.

For some reason, I had always associated Ziggurat with Leiningen. I had never questioned whether that was actually a requirement or simply the build tool chosen by the original project. Once the idea was planted, I started reading documentation, experimenting with Gradle plugins for Clojure, and going through the familiar cycle of trial and error. After several iterations, I finally managed to build a small Gradle bootstrap capable of compiling both languages and launching a standard Ziggurat application without relying on Leiningen at all.

The resulting architecture feels much more natural than what I originally built with Borobudur. The application entry point remains in Clojure, exactly as Ziggurat expects, while the Kafka handlers and business logic are implemented in Java. Gradle manages the build, dependency resolution, and project structure, allowing me to use the same workflow I use for every other Java application.

One of the biggest advantages of this approach is that I no longer need to expose Ziggurat APIs through a wrapper. Since the application itself is still a normal Ziggurat application, every feature provided by the framework is immediately available. There is no additional abstraction layer to maintain whenever Ziggurat evolves.

The overall codebase is also much smaller. Borobudur contains infrastructure whose primary responsibility is bridging Java and Clojure in the opposite direction. That infrastructure simply disappears when the application starts from Clojure instead. The architecture becomes easier to understand because there is less code whose sole purpose is interoperability.

Perhaps the biggest improvement, however, is the development experience. Since Gradle is responsible for dependency management, importing generated Protobuf classes, internal service clients, or any third-party library feels exactly like working in a normal Java project. Most of the application remains ordinary Java code, while Clojure is responsible only for bootstrapping Ziggurat and wiring everything together.

Ironically, this also made my original task much easier. Migrating the metrics implementation from StatsD to Prometheus was almost entirely a Java exercise. I barely had to touch the Clojure portion of the application because it existed only as a lightweight bootstrap around Ziggurat. That was precisely the developer experience I had been hoping for when I first started working on Borobudur.

Looking Back

Do I regret building Borobudur? Not at all.

Without Borobudur, I probably would never have gained a deep enough understanding of how Java and Clojure interact on the JVM to arrive at this alternative architecture. The wrapper solved a real problem, and it continues to serve production workloads successfully. More importantly, building it forced me to learn the internals of Ziggurat and Clojure interoperability, knowledge that ultimately led me to a simpler solution.

The most valuable lesson from this experience was not about Gradle, Leiningen, Java, or even Clojure. It was about recognizing how easily our own assumptions can limit the solutions we consider. For months, I kept asking myself, “How can Java run a Clojure application?” I never stopped to ask the much simpler question, “Why not let Clojure run Java instead?”

Sometimes the biggest breakthrough does not come from learning a new technology. It comes from looking at the same problem from the opposite direction.