Understanding Elixir's List.to_string

So I came across this (quite old) post on stackoverflow, someone wanted to print a list next to a string and he was struggling with it

16

I would like to print a list along with a string identifier like

list = [1, 2, 3]
IO.puts "list is ", list

This does not work. I have tried few variations like

# this prints only the list, not any strings
IO.inspect list
# using puts which also does

and I was a bit baffled as of why this is an issue at all, just convert your list to a string and print it, this logic should work in any language in any print function, should be simple and straightforward

Elixir surely have a function that convert a lists to string, and it does List.to_string

But to my surprise List.to_string was doing weird things

iex(42)> [232, 137, 178] |> List.to_string
<<195, 168, 194, 137, 194, 178>>
iex(43)> [1, 2, 3] |> List.to_string
<<1, 2, 3>>

That was not what I was expecting, I was expecting a returned value that is similar to what IO.inspect produce

So to verify my expectation, I checked what clojure does, because clojure to me is the epitome of sanity

Clojure 1.12.4
user=> (str '(232 137 178))
"(232 137 178)"
user=> (str '(1 2 3))
"(1 2 3)"

it was more or less what IO.inspect does
it returns something that look like how lists "look like" as code

So next step was more introspection

iex(47)> [232, 137, 178] |> List.to_string |> i
Term
  <<195, 168, 194, 137, 194, 178>>
Data type
  BitString
Byte size
  6
Description
  This is a string: a UTF-8 encoded binary. It's printed with the `<<>>`
  syntax (as opposed to double quotes) because it contains non-printable
  UTF-8 encoded code points (the first non-printable code point being
  `<<194, 137>>`).
Reference modules
  String, :binary
Implemented protocols
  Collectable, IEx.Info, Inspect, JSON.Encoder, List.Chars, String.Chars

What the heck is BitString?

So I kept fiddling with the function, until I finally got it

iex(44)> ["o","m",["z","y"]] |> List.to_string
"omzy"
iex(45)> ["o","m",["z","y"]] |> IO.inspect
["o", "m", ["z", "y"]]
["o", "m", ["z", "y"]]

List.to_string , does not transform a list to a string (preserving its structure), List.to_string flattens a list, take each element and transform it to a UTF-8 code point, and if you try to print that, you will get whatever string those codes points produce

iex(49)> [232, 137, 178] |> List.to_string |> IO.puts
è²
:ok
iex(50)> [91, 50, 51, 50, 44, 32, 49, 51, 55, 44, 32, 49, 55, 56, 93] |> List.to_string |> IO.puts
[232, 137, 178]
:ok

In retrospect this is what the docs says



but well, the docs wasn't telling what I wanted it to say 😀

And oh, before I forget, Elixir have the inspect function, which does exactly was I thought List.to_string would do

iex(53)> [232, 137, 178] |> inspect |> IO.puts
[232, 137, 178]
:ok
iex(54)> ["o","m",["z","y"]] |> inspect |> IO.puts
["o", "m", ["z", "y"]]
:ok

Permalink

Statistics made simple

I have a weird relationship with statistics: on one hand, I try not to look at it too often. Maybe once or twice a year. It’s because analytics is not actionable: what difference does it make if a thousand people saw my article or ten thousand?

I mean, sure, you might try to guess people’s tastes and only write about what’s popular, but that will destroy your soul pretty quickly.

On the other hand, I feel nervous when something is not accounted for, recorded, or saved for future reference. I might not need it now, but what if ten years later I change my mind?

Seeing your readers also helps to know you are not writing into the void. So I really don’t need much, something very basic: the number of readers per day/per article, maybe, would be enough.

Final piece of the puzzle: I self-host my web projects, and I use an old-fashioned web server instead of delegating that task to Nginx.

Static sites are popular and for a good reason: they are fast, lightweight, and fulfil their function. I, on the other hand, might have an unfinished gestalt or two: I want to feel the full power of the computer when serving my web pages, to be able to do fun stuff that is beyond static pages. I need that freedom that comes with a full programming language at your disposal. I want to program my own web server (in Clojure, sorry everybody else).

Existing options

All this led me on a quest for a statistics solution that would uniquely fit my needs. Google Analytics was out: bloated, not privacy-friendly, terrible UX, Google is evil, etc.

What is going on?

Some other JS solution might’ve been possible, but still questionable: SaaS? Paid? Will they be around in 10 years? Self-host? Are their cookies GDPR-compliant? How to count RSS feeds?

Nginx has access logs, so I tried server-side statistics that feed off those (namely, Goatcounter). Easy to set up, but then I needed to create domains for them, manage accounts, monitor the process, and it wasn’t even performant enough on my server/request volume!

My solution

So I ended up building my own. You are welcome to join, if your constraints are similar to mine. This is how it looks:

It’s pretty basic, but does a few things that were important to me.

Setup

Extremely easy to set up. And I mean it as a feature.

Just add our middleware to your Ring stack and get everything automatically: collecting and reporting.

(def app
  (-> routes
    ...
    (ring.middleware.params/wrap-params)
    (ring.middleware.cookies/wrap-cookies)
    ...
    (clj-simple-stats.core/wrap-stats))) ;; <-- just add this

It’s zero setup in the best sense: nothing to configure, nothing to monitor, minimal dependency. It starts to work immediately and doesn’t ask anything from you, ever.

See, you already have your web server, why not reuse all the setup you did for it anyway?

Request types

We distinguish between request types. In my case, I am only interested in live people, so I count them separately from RSS feed requests, favicon requests, redirects, wrong URLs, and bots. Bots are particularly active these days. Gotta get that AI training data from somewhere.

RSS feeds are live people in a sense, so extra work was done to count them properly. Same reader requesting feed.xml 100 times in a day will only count as one request.

Hosted RSS readers often report user count in User-Agent, like this:

Feedly/1.0 (+http://www.feedly.com/fetcher.html; 457 subscribers; like FeedFetcher-Google)

Mozilla/5.0 (compatible; BazQux/2.4; +https://bazqux.com/fetcher; 6 subscribers)

Feedbin feed-id:1373711 - 142 subscribers

My personal respect and thank you to everybody on this list. I see you.

Graphs

Visualization is important, and so is choosing the correct graph type. This is wrong:

Continuous line suggests interpolation. It reads like between 1 visit at 5am and 11 visits at 6am there were points with 2, 3, 5, 9 visits in between. Maybe 5.5 visits even! That is not the case.

This is how a semantically correct version of that graph should look:

Some attention was also paid to having reasonable labels on axes. You won’t see something like 117, 234, 10875. We always choose round numbers appropriate to the scale: 100, 200, 500, 1K etc.

Goes without saying that all graphs have the same vertical scale and syncrhonized horizontal scroll.

Insights

We don’t offer much (as I don’t need much), but you can narrow reports down by page, query, referrer, user agent, and any date slice.

Not implemented (yet)

It would be nice to have some insights into “What was this spike caused by?”

Some basic breakdown by country would be nice. I do have IP addresses (for what they are worth), but I need a way to package GeoIP into some reasonable size (under 1 Mb, preferably; some loss of resolution is okay).

Finally, one thing I am really interested in is “Who wrote about me?” I do have referrers, only question is how to separate signal from noise.

Performance. DuckDB is a sport: it compresses data and runs column queries, so storing extra columns per row doesn’t affect query performance. Still, each dashboard hit is a query across the entire database, which at this moment (~3 years of data) sits around 600 MiB. I definitely need to look into building some pre-calculated aggregates.

One day.

How to get

Head to github.com/tonsky/clj-simple-stats and follow the instructions:

Let me know what you think! Is it usable to you? What could be improved?

Permalink

Call for Proposals. Feb. 2026 Member Survey

Greetings folks!

Clojurists Together is pleased to announce that we are opening our Q2 2026 funding round for Clojure Open Source Projects. Applications will be accepted through the 19th of March 2026 (midnight Pacific Time). We are looking forward to reviewing your proposals! More information and the application can be found here.

We will be awarding up to $33,000 USD for a total of 5-7 projects. The $2k funding tier is for experimental projects or smaller proposals, whereas the $9k tier is for those that are more established. Projects generally run 3 months, however, the $9K projects can run between 3 and 12 months as needed. We expect projects to start at the beginning of April 2026.

A BIG THANKS to all our members for your continued support. We also want to encourage you to reach out to your colleagues and companies to join Clojurists Together so that we can fund EVEN MORE great projects throughout the year.

And Now the Survey…

We surveyed the community in February to find out what what issues were top of mind and types of initiatives they would like us to focus on for this round of funding. As always, there were a lot of ideas and we hope they will be useful in informing your project proposals.

A number of themes appeared in the survey results.

The biggest theme, by far, was related to adoption and growth of Clojure. Respondents repeatedly mentioned that Clojure is niche, and although they are happy with Clojure, that makes it harder to justify for projects, to find employment, and to persuade others than it is for more popular languages. Respondents want a larger community and wider adoption. In particular, they want more public advocacy for Clojure, including videos, tutorials, public success stories, starter projects, and outreach in general.

Another major theme was AI. Respondents were concerned about AI coding assistants being perceived as having weak Clojure support, and they expressed frustration that Python is perceived as the safe choice for AI despite how well Clojure works with AI tooling. Nonetheless, respondents would like to see more work on tooling, guides, and resources for using AI with Clojure.

ClojureScript and JavaScript interop received the most specific attention. Respondents want CLJS/Cherry/Skittle to provide frictionless support for modern JavaScript standards (ES6 and ESM) and would like an overall simplification of the build and run process.

Developer experience issues came up a number of times, including: confusing error messages, poor documentation, and under-supported libraries. Improvements to any of those would be welcome.

Difficulty finding Clojure employment was another recurring theme. Respondents were not sure how to solve it, but suggested a community job board might be helpful.


February 2026 Survey

88% of respondents use or refer to projects funded by Clojurists Together

time spent q2 2026


communication q2 2026


platform q2 survey


clojure improvements q2 2026


CLojure script improvement q2 2026


Plans for Conference Attendance in 2026 (number of mentions):

  • Clojure/Conj: 8
  • Dutch Clojure Days: 6
  • babashka conference: 5
  • Clojure South: 1
  • Clojure Jam: 1
  • reClojure: 1

If you were only to name ONE, what is the biggest challenge facing Clojure developers today and how can Clojurists Together support you or your organization in addressing those challenges? If you could wave a magic wand and change anything inside the Clojure community, what would it be? (select responses by category).

Adoption:

  • Advertising video like that one on Clojure Conj five years ago or so
  • Language Adoption and popularity, projects that helps to grow the popularity of the language or helps to start programming easily
  • Lack of widespread adoption is not a problem… until you want to convince others that Clojure is a technology you can count on and is worth developing with. Convincing others that Clojure is a great and solid technology that’s here to stay, regardless of low(er) adoption, is sometimes tough.
  • The fact that many teams and project would rule out Clojure as an option, being perceived as niche, far from mainstream, and thus risky
  • I would have more Clojure evangelism. More videos/blog posts/demos around using Clojure, both about whatever is currently at the peak of the broader tech hype cycle – LLMs currently – as well as uses and topics outside of the hype cycle.

AI/LLMs

  • Keeping relevant in a programming market is the top challenge. With the IA, everyone is moving toward the most popular languages. If nobody uses Clojure, it is more difficult to justify its use, no matter how much better it could be.
  • The biggest challenge is the spreading expectation that everything will be done in Python because AI will fix whatever problems Python will allegedly cause.
  • How will Clojure and the Community fare in the light of LLMs and coding assistants?
  • We are being encouraged to use Agentic AI coding assistants, but their support for Clojure is behind that of other languages.
  • How can we articulate the value of Clojure as a sustainable, modern solution when discussion about AI is taking all of the air in the room. We can for example fortify our tooling regards this. Projects such as Calva make Clojure easy to approach for newbies and ClojureMCP is a great tool for Agentic developers.
  • I am unsure about how LLM driven development fits with Clojure. I find myself building some things with JS and Python. For larger projects I am relying on Clojure for it’s correctness properties and lower likelyhood of bugs.
  • Support AI integration Clojure projects

Employment

  • Difficulty finding interesting and reliable work, but this isn’t just Clojure-specific, the whole industry is weird right now.
  • Finding employment writing Clojure code
  • I would say that the biggest challenge for Clojure developers is in the job search. I’m not certain of a solution to this challenge, but perhaps some kind of Clojurists Together Job Board?

Developer Experience

  • Developer tooling improvements competitive with modern JavaScript/TypeScript tooling
  • Missing parts of the data science stack
  • Better documentation of the tools and projects and more tutorials
  • Better integration with cljs/scittle/js/typescript - separate cljs compilation too complicated - scittle/squint/cherry with ES6 integration is the way clojurescript support for ESM libraries. It’s crazy the hoops you have to jump through to use ESM with clojurescript, most people probably assume it’s not possible at all because it’s so difficult.
  • Closer integration with JavaScript/TypeScript tooling -Seamless integration of cljs/cherry/scittle into the js-ecosystem with live repl and load-file support, standard sente/websocket communication included, standard/default solid telemetry/instrumentation API
  • Quicker resolution of outstanding Clojure (JIRA) issues
  • Have a official support program to people that focus on promote the language and/or community instead of library maintainers (like GDE from Google, MVP from Microsoft, Github Stars from Github)
  • I would encourage “cljc” as a default idiom. The linter could say, “This could be a cljc file!” or “Change this to that and suddenly it would be cljc-compatible”.
  • It remains Error Reporting imho, and anyone working on improving it would get my eternal gratitude.

What areas of the Clojure ecosystem need support? (select responses)

  • “I think something around marketing/evangelism; I have worked on several teams using Clojure/ClojureScript that have had to defend the use of Clojure/ClojureScript against more mainstream JVM/JS languages, and the core issue we’ve run up against is a confluence of the following three items:
    – 1. there are more Kotlin/Scala/TypeScript/Java developers than there are Clojure/ClojureScript developers
    – 2. The salary ranges for those languages tends to be lower than that for Clojure/ClojureScript
    – 3. The greatest benefits to be gained from using Clojure/ClojureScript – systems which are far easier to understand, maintain, and extend, thus accelerating business goals – are exceptionally difficult to quantify.”
  • “data.xml – My ticket has been rotting away for 14 months. :) (XML is a core technology at my company.)”
  • Repl tooling and setup, more official tutorials and guides. Data validation and schemas.
  • Data science, clojure for frontend
  • Guides for LLM driven development that don’t invoke huge piles of software just to modify code.
  • Growth to new domains and use cases, specifically scientific / academic / teaching

Permalink

What’s Next for clojure-mode?

Good news, everyone! clojure-mode 5.22 is out with many small improvements and bug fixes!

While TreeSitter is the future of Emacs major modes, the present remains a bit more murky – not everyone is running a modern Emacs or an Emacs built with TreeSitter support, and many people have asked that “classic” major modes continue to be improved and supported alongside the newer TS-powered modes (in our case – clojure-ts-mode). Your voices have been heard! On Bulgaria’s biggest national holiday (Liberation Day), you can feel liberated from any worries about the future of clojure-mode, as it keeps getting the love and attention that it deserves! Looking at the changelog – 5.22 is one of the biggest releases in the last few years and I hope you’ll enjoy it!1

Now let me walk you through some of the highlights.

edn-mode Gets Some Love

edn-mode has always been the quiet sibling of clojure-mode – a mode for editing EDN files that was more of an afterthought than a first-class citizen. That changed with 5.21 and the trend continues in 5.22. The mode now has its own dedicated keymap with data-appropriate bindings, meaning it no longer inherits code refactoring commands that make no sense outside of Clojure source files. Indentation has also been corrected – paren lists in EDN are now treated as data (which they are), not as function calls.

Small things, sure, but they add up to a noticeably better experience when you’re editing config files, test fixtures, or any other EDN data.

Font-locking Updated for Clojure 1.12

Font-locking has been updated to reflect Clojure 1.12’s additions – new built-in dynamic variables and core functions are now properly highlighted. The optional clojure-mode-extra-font-locking package covers everything from 1.10 through 1.12, including bundled namespaces and clojure.repl forms.2 Some obsolete entries (like specify and specify!) have been cleaned up as well.

On a related note, protocol method docstrings now correctly receive font-lock-doc-face styling, and letfn binding function names get proper font-lock-function-name-face treatment. These are the kind of small inconsistencies that you barely notice until they’re fixed, and then you wonder how you ever lived without them.

Discard Form Styling

A new clojure-discard-face has been added for #_ reader discard forms. By default it inherits from the comment face, so discarded forms visually fade into the background – exactly what you’d expect from code that won’t be read. Of course, you can customize the face to your liking.

Notable Bug Fixes

A few fixes that deserve a special mention:

  • clojure-sort-ns no longer corrupts non-sortable forms – previously, sorting a namespace that contained :gen-class could mangle it. That’s fixed now.
  • clojure-thread-last-all and line comments – the threading refactoring command was absorbing closing parentheses into line comments. Not anymore.
  • clojure-update-ns works again – this one had been quietly broken and is now restored to full functionality.
  • clojure-add-arity preserves arglist metadata – when converting from single-arity to multi-arity, metadata on the argument vector is no longer lost.

The Road Ahead

So, what’s actually next for clojure-mode? The short answer is: more of the same. clojure-mode will continue to receive updates, bug fixes, and improvements for the foreseeable future. There is no rush for anyone to switch to clojure-ts-mode, and no plans to deprecate the classic mode anytime soon.

That said, if you’re curious about clojure-ts-mode, its main advantage right now is performance. TreeSitter-based font-locking and indentation are significantly faster than the regex-based approach in clojure-mode. If you’re working with very large Clojure files and noticing sluggishness, it’s worth giving clojure-ts-mode a try. My guess is that most people won’t notice a meaningful difference in everyday editing, but your mileage may vary.

The two modes will coexist for as long as it makes sense. Use whichever one works best for you – they’re both maintained by the same team (yours truly and co) and they both have a bright future ahead of them. At least I hope so!

As usual - big thanks to everyone supporting my Clojure OSS work, especially the members of Clojurists Together! You rock!

That’s all I have for you today. Keep hacking!

  1. I also hope I didn’t break anything. :-) 

  2. I wonder if anyone’s using this package, though. For me CIDER’s font-locking made it irrelevant a long time ago. 

Permalink

I am sorry, but everyone is getting syntax highlighting wrong

Translations: Russian

Syntax highlighting is a tool. It can help you read code faster. Find things quicker. Orient yourself in a large file.

Like any tool, it can be used correctly or incorrectly. Let’s see how to use syntax highlighting to help you work.

Christmas Lights Diarrhea

Most color themes have a unique bright color for literally everything: one for variables, another for language keywords, constants, punctuation, functions, classes, calls, comments, etc.

Sometimes it gets so bad one can’t see the base text color: everything is highlighted. What’s the base text color here?

The problem with that is, if everything is highlighted, nothing stands out. Your eye adapts and considers it a new norm: everything is bright and shiny, and instead of getting separated, it all blends together.

Here’s a quick test. Try to find the function definition here:

and here:

See what I mean?

So yeah, unfortunately, you can’t just highlight everything. You have to make decisions: what is more important, what is less. What should stand out, what shouldn’t.

Highlighting everything is like assigning “top priority” to every task in Linear. It only works if most of the tasks have lesser priorities.

If everything is highlighted, nothing is highlighted.

Enough colors to remember

There are two main use-cases you want your color theme to address:

  1. Look at something and tell what it is by its color (you can tell by reading text, yes, but why do you need syntax highlighting then?)
  2. Search for something. You want to know what to look for (which color).

1 is a direct index lookup: color → type of thing.

2 is a reverse lookup: type of thing → color.

Truth is, most people don’t do these lookups at all. They might think they do, but in reality, they don’t.

Let me illustrate. Before:

After:

Can you see it? I misspelled return for retunr and its color switched from red to purple.

I can’t.

Here’s another test. Close your eyes (not yet! Finish this sentence first) and try to remember what color your color theme uses for class names?

Can you?

If the answer for both questions is “no”, then your color theme is not functional. It might give you comfort (as in—I feel safe. If it’s highlighted, it’s probably code) but you can’t use it as a tool. It doesn’t help you.

What’s the solution? Have an absolute minimum of colors. So little that they all fit in your head at once. For example, my color theme, Alabaster, only uses four:

  • Green for strings
  • Purple for constants
  • Yellow for comments
  • Light blue for top-level definitions

That’s it! And I was able to type it all from memory, too. This minimalism allows me to actually do lookups: if I’m looking for a string, I know it will be green. If I’m looking at something yellow, I know it’s a comment.

Limit the number of different colors to what you can remember.

If you swap green and purple in my editor, it’ll be a catastrophe. If somebody swapped colors in yours, would you even notice?

What should you highlight?

Something there isn’t a lot of. Remember—we want highlights to stand out. That’s why I don’t highlight variables or function calls—they are everywhere, your code is probably 75% variable names and function calls.

I do highlight constants (numbers, strings). These are usually used more sparingly and often are reference points—a lot of logic paths start from constants.

Top-level definitions are another good idea. They give you an idea of a structure quickly.

Punctuation: it helps to separate names from syntax a little bit, and you care about names first, especially when quickly scanning code.

Please, please don’t highlight language keywords. class, function, if, elsestuff like this. You rarely look for them: “where’s that if” is a valid question, but you will be looking not at the if the keyword, but at the condition after it. The condition is the important, distinguishing part. The keyword is not.

Highlight names and constants. Grey out punctuation. Don’t highlight language keywords.

Comments are important

The tradition of using grey for comments comes from the times when people were paid by line. If you have something like

of course you would want to grey it out! This is bullshit text that doesn’t add anything and was written to be ignored.

But for good comments, the situation is opposite. Good comments ADD to the code. They explain something that couldn’t be expressed directly. They are important.

So here’s another controversial idea:

Comments should be highlighted, not hidden away.

Use bold colors, draw attention to them. Don’t shy away. If somebody took the time to tell you something, then you want to read it.

Two types of comments

Another secret nobody is talking about is that there are two types of comments:

  1. Explanations
  2. Disabled code

Most languages don’t distinguish between those, so there’s not much you can do syntax-wise. Sometimes there’s a convention (e.g. -- vs /* */ in SQL), then use it!

Here’s a real example from Clojure codebase that makes perfect use of two types of comments:

Disabled code is gray, explanation is bright yellow

Light or dark?

Per statistics, 70% of developers prefer dark themes. Being in the other 30%, that question always puzzled me. Why?

And I think I have an answer. Here’s a typical dark theme:

and here’s a light one:

On the latter one, colors are way less vibrant. Here, I picked them out for you:

Notice how many colors there are. No one can remember that many.

This is because dark colors are in general less distinguishable and more muddy. Look at Hue scale as we move brightness down:

Basically, in the dark part of the spectrum, you just get fewer colors to play with. There’s no “dark yellow” or good-looking “dark teal”.

Nothing can be done here. There are no magic colors hiding somewhere that have both good contrast on a white background and look good at the same time. By choosing a light theme, you are dooming yourself to a very limited, bad-looking, barely distinguishable set of dark colors.

So it makes sense. Dark themes do look better. Or rather: light ones can’t look good. Science ¯\_(ツ)_/¯

But!

But.

There is one trick you can do, that I don’t see a lot of. Use background colors! Compare:

The first one has nice colors, but the contrast is too low: letters become hard to read.

The second one has good contrast, but you can barely see colors.

The last one has both: high contrast and clean, vibrant colors. Lighter colors are readable even on a white background since they fill a lot more area. Text is the same brightness as in the second example, yet it gives the impression of clearer color. It’s all upside, really.

UI designers know about this trick for a while, but I rarely see it applied in code editors:

If your editor supports choosing background color, give it a try. It might open light themes for you.

Bold and italics

Don’t use. This goes into the same category as too many colors. It’s just another way to highlight something, and you don’t need too many, because you can’t highlight everything.

In theory, you might try to replace colors with typography. Would that work? I don’t know. I haven’t seen any examples.

Using italics and bold instead of colors

Myth of number-based perfection

Some themes pay too much attention to be scientifically uniform. Like, all colors have the same exact lightness, and hues are distributed evenly on a circle.

This could be nice (to know if you have OCD), but in practice, it doesn’t work as well as it sounds:

OkLab l=0.7473 c=0.1253 h=0, 45, 90, 135, 180, 225, 270, 315

The idea of highlighting is to make things stand out. If you make all colors the same lightness and chroma, they will look very similar to each other, and it’ll be hard to tell them apart.

Our eyes are way more sensitive to differences in lightness than in color, and we should use it, not try to negate it.

Let’s design a color theme together

Let’s apply these principles step by step and see where it leads us. We start with the theme from the start of this post:

First, let’s remove highlighting from language keywords and re-introduce base text color:

Next, we remove color from variable usage:

and from function/method invocation:

The thinking is that your code is mostly references to variables and method invocation. If we highlight those, we’ll have to highlight more than 75% of your code.

Notice that we’ve kept variable declarations. These are not as ubiquitous and help you quickly answer a common question: where does thing thing come from?

Next, let’s tone down punctuation:

I prefer to dim it a little bit because it helps names stand out more. Names alone can give you the general idea of what’s going on, and the exact configuration of brackets is rarely equally important.

But you might roll with base color punctuation, too:

Okay, getting close. Let’s highlight comments:

We don’t use red here because you usually need it for squiggly lines and errors.

This is still one color too many, so I unify numbers and strings to both use green:

Finally, let’s rotate colors a bit. We want to respect nesting logic, so function declarations should be brighter (yellow) than variable declarations (blue).

Compare with what we started:

In my opinion, we got a much more workable color theme: it’s easier on the eyes and helps you find stuff faster.

Shameless plug time

I’ve been applying these principles for about 8 years now.

I call this theme Alabaster and I’ve built it a couple of times for the editors I used:

It’s also been ported to many other editors and terminals; the most complete list is probably here. If your editor is not on the list, try searching for it by name—it might be built-in already! I always wondered where these color themes come from, and now I became an author of one (and I still don’t know).

Feel free to use Alabaster as is or build your own theme using the principles outlined in the article—either is fine by me.

As for the principles themselves, they worked out fantastically for me. I’ve never wanted to go back, and just one look at any “traditional” color theme gives me a scare now.

I suspect that the only reason we don’t see more restrained color themes is that people never really thought about it. Well, this is your wake-up call. I hope this will inspire people to use color more deliberately and to change the default way we build and use color themes.

Permalink

How To Cleanly Integrate Java and Clojure In The Same Package

A hybrid Java/Clojure library designed to demonstrate how to setup Java interop using Maven

This is a complete Maven-first Clojure/Java interop application. It details how to create a Maven application, enrich it with clojure code, call into clojure from Java, and hook up the entry points for both Java and Clojure within the same project.

Further, it contains my starter examples of using the fantastic Incanter Statistical and Graphics Computing Library in clojure. I include both a pom.xml and a project.clj showing how to pull in the dependencies.

The outcome is a consistent maven-archetyped project, wherein maven and leiningen play nicely together. This allows the best of both ways to be applied together. For the emacs user, I include support for cider and swank. NRepl by itself is present for general purpose use as well.

Starting a project

Maven first

Create Maven project

follow these steps

mvn archetype:generate -DgroupId=com.mycompany.app -DartifactId=my-app -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false

cd my-app

mvn package

java -cp target/my-app-1.0-SNAPSHOT.jar com.mycompany.app.App
Hello World

Add Clojure code

Create a clojure core file

mkdir -p src/main/clojure/com/mycompany/app

touch src/main/clojure/com/mycompany/app/core.clj

Give it some goodness…

(ns com.mycompany.app.core
(:gen-class)
(:use (incanter core stats charts)))

(defn -main [& args]
(println "Hello Clojure!")
(println "Java main called clojure function with args: "
(apply str (interpose " " args))))

(defn run []
(view (histogram (sample-normal 1000))))

Notice that we’ve added in the Incanter Library and made a run function to pop up a histogram of sample data

Add dependencies to your pom.xml

<dependencies>
<dependency>
<groupId>org.clojure</groupId>
<artifactId>clojure</artifactId>
<version>1.7.0</version>
</dependency>
<dependency>
<groupId>org.clojure</groupId>
<artifactId>clojure-contrib</artifactId>
<version>1.2.0</version>
</dependency>
<dependency>
<groupId>incanter</groupId>
<artifactId>incanter</artifactId>
<version>1.9.0</version>
</dependency>
<dependency>
<groupId>org.clojure</groupId>
<artifactId>tools.nrepl</artifactId>
<version>0.2.10</version>
</dependency>
<!-- pick your poison swank or cider. just make sure the version of nRepl matches. -->
<dependency>
<groupId>cider</groupId>
<artifactId>cider-nrepl</artifactId>
<version>0.10.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>swank-clojure</groupId>
<artifactId>swank-clojure</artifactId>
<version>1.4.3</version>
</dependency>
</dependencies>

Java main class

Modify your java main to call your clojure main like in the following:

package com.mycompany.app;

// for clojure's api
import clojure.lang.IFn;
import clojure.java.api.Clojure;

// for my api
import clojure.lang.RT;

public class App
{
public static void main( String[] args )
{

System.out.println("Hello Java!" );

try {

// running my clojure code
RT.loadResourceScript("com/mycompany/app/core.clj");
IFn main = RT.var("com.mycompany.app.core", "main");
main.invoke(args);

// running the clojure api
IFn plus = Clojure.var("clojure.core", "+");
System.out.println(plus.invoke(1, 2).toString());

} catch(Exception e) {
e.printStackTrace();
}

}
}

Maven plugins for building

You should add in these plugins to your pom.xml

  • Add the maven-assembly-plugin

    Create an Ubarjar

    Bind the maven-assembly-plugin to the package phase this will create a jar file without the dependencies suitable for deployment to a container with deps present.

  <plugin>
    <artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>

<!-- use clojure main -->
<!-- <mainClass>com.mycompany.app.core</mainClass> -->

<!-- use java main -->
<mainClass>com.mycompany.app.App</mainClass>

</manifest>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
        <phase>package</phase>
        <goals>
<goal>single</goal>
</goals>
</execution>
</executions>
  </plugin>
  • Add the clojure-maven-plugin

    Add this plugin to give your project the mvn: clojure:… commands

    A full list of these is posted later in this article.

  •   <plugin>
        <groupId>com.theoryinpractise</groupId>
    <artifactId>clojure-maven-plugin</artifactId>
    <version>1.7.1</version>
    <configuration>
    <mainClass>com.mycompany.app.core</mainClass>
    </configuration>
    <executions>
    <execution>
    <id>compile-clojure</id>
            <phase>compile</phase>
            <goals>
    <goal>compile</goal>
    </goals>
    </execution>
    <execution>
    <id>test-clojure</id>
            <phase>test</phase>
            <goals>
    <goal>test</goal>
    </goals>
    </execution>
    </executions>
      </plugin>
    
  • Add the maven-compiler-plugin

    Add Java version targeting

    This is always good to have if you are working against multiple versions of Java.

  •   <plugin>
        <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.3</version>
    <configuration><source>1.8</source>
    <target>1.8</target>
    </configuration>
      </plugin>
    
  • Add the maven-exec-plugin

    Add this plugin to give your project the mvn exec:… commands

    The maven-exec-plugin is nice for running your project from the commandline, build scripts, or from inside an IDE.

  •   <plugin>
        <groupId>org.codehaus.mojo</groupId>
    <artifactId>exec-maven-plugin</artifactId>
    <version>1.4.0</version>
    <executions>
    <execution>
    <goals>
    <goal>exec</goal>
    </goals>
    </execution>
    </executions>
    <configuration>
    <mainClass>com.mycompany.app.App</mainClass>
    </configuration>
      </plugin>
    
  • Add the maven-jar-plugin

    With this plugin you can manipulate the manifest of your default package. In this case, I’m not adding a main, because I’m using the uberjar above with all the dependencies for that. However, I included this section for cases, where the use case is for a non-stand-alone assembly.

  •   <plugin>
        <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <version>2.6</version>
    <configuration>
    <archive>
    <manifest>
    
    <!-- use clojure main -->
    <!-- <mainClass>com.mycompany.app.core</mainClass> -->
    
    <!-- use java main -->
    <!-- <mainClass>com.mycompany.app.App</mainClass> -->
    
    </manifest>
    </archive>
    </configuration>
      </plugin>
    

    Using Maven

    • building
    mvn package
    
    • Run from cli with
      • run from java entry point:

    java -cp target/my-app-1.0-SNAPSHOT-jar-with-dependencies.jar com.mycompany.app.App
    
  • Run from Clojure entry point:
  • java -cp target/my-app-1.0-SNAPSHOT-jar-with-dependencies.jar com.mycompany.app.core
    
  • Run with entry point specified in uberjar MANIFEST.MF:
  • java -jar target/my-app-1.0-SNAPSHOT-jar-with-dependencies.jar
    
  • Run from maven-exec-plugin
    • With plugin specified entry point:
  • mvn exec:java
    
  • Specify your own entry point:
    • Java main
  • mvn exec:java -Dexec.mainClass="com.mycompany.app.App"
    
  • Clojure main
  • mvn exec:java -Dexec.mainClass="com.mycompany.app.core"
    
  • Feed args with this directive
  • -Dexec.args="foo"
    
  • Run with maven-clojure-plugin
    • Clojure main
  • mvn clojure:run
    
  • Clojure test
    • Add a test

      In order to be consistent with the test location convention in maven, create a path and clojure test file like this:

  • mkdir src/test/clojure/com/mycompany/app
    
    touch src/test/clojure/com/mycompany/app/core_test.clj
    

    Add the following content:

    (ns com.mycompany.app.core-test
    (:require [clojure.test :refer :all]
    [com.mycompany.app.core :refer :all]))
    
    (deftest a-test
    (testing "Rigourous Test :-)"
    (is (= 0 0))))
    
  • Testing
  • mvn clojure:test
    

    Or

    mvn clojure:test-with-junit
    
  • Available Maven clojure:… commands

    Here is the full set of options available from the clojure-maven-plugin:

  • mvn ...
    
    clojure:add-source
    clojure:add-test-source
    clojure:compile
    clojure:test
    clojure:test-with-junit
    clojure:run
    clojure:repl
    clojure:nrepl
    clojure:swank
    clojure:nailgun
    clojure:gendoc
    clojure:autodoc
    clojure:marginalia
    

    See documentation:

    Add Leiningen support

    • Create project.clj

      Next to your pom.xml, create the Clojure project file

    touch project.clj
    

    Add this content

    (defproject my-sandbox "1.0-SNAPSHOT"
    :description "My Encanter Project"
    :url "http://joelholder.com"
    :license {:name "Eclipse Public License"
    :url "http://www.eclipse.org/legal/epl-v10.html"}
    :dependencies [[org.clojure/clojure "1.7.0"]
    [incanter "1.9.0"]]
    :main com.mycompany.app.core
    :source-paths ["src/main/clojure"]
    :java-source-paths ["src/main/java"]
    :test-paths ["src/test/clojure"]
    :resource-paths ["resources"]
    :aot :all)
    

    Note that we’ve set the source code and test paths for both java and clojure to match the maven-way of doing this.

    This gives us a consistent way of hooking the code from both lein and mvn. Additionally, I’ve added the incanter library here. The dependency should be expressed in the project file, because when we run nRepl from this directory, we want it to be available in our namespace, i.e. com.mycompany.app.core

  • Run with Leiningen
  • lein run
    
  • Test with Leiningen
  • lein test
    

    Running with org-babel

    This blog entry was exported to html from the README.org of this project. It sits in the base directory of the project. By using it to describe the project and include executable blocks of code from the project itself, we’re able to provide working examples of how to use the library in it’s documentation. People can simply clone our project and try out the library by executing it’s documentation. Very nice..

    Make sure you jack-in to cider first:

    M-x cider-jack-in (Have it mapped to F9 in my emacs)

    Clojure code

    The Clojure code block

    #+begin_src clojure :tangle ./src/main/clojure/com/mycompany/app/core.clj :results output 
      (-main)
      (run)
    #+end_src
    

    Blocks are run in org-mode with C-c C-c

    (-main)
    (run)
    
    Hello Clojure!
    Java main called clojure function with args:
    

    Note that we ran both our main and run functions here. -main prints out the text shown above. The run function actually opens the incanter java image viewer and shows us a picture of our graph.

    run.png

    I have purposefully not invested in styling these graphs in order to keep the code examples simple and focussed, however incanter makes really beautiful output. Here’s a link to get you started:

    http://incanter.org/

    Playing with Incanter

    (use '(incanter core charts pdf))
    ;;; Create the x and y data:
    (def x-data [0.0 1.0 2.0 3.0 4.0 5.0])
    (def y-data [2.3 9.0 2.6 3.1 8.1 4.5])
    (def xy-line (xy-plot x-data y-data))
    (view xy-line)
    (save-pdf xy-line "img/incanter-xy-line.pdf")
    (save xy-line "img/incanter-xy-line.png")
    

    PNG

    incanter-xy-line.png

    Resources

    Finally here are some resources to move you along the journey. I drew on the links cited below along with a night of hacking to arrive a nice clean interop skeleton. Feel free to use my code available here:

    https://github.com/jclosure/my-app

    For the eager, here is a link to my full pom:

    https://github.com/jclosure/my-app/blob/master/pom.xml

    Working with Apache Storm (multilang)

    Starter project:

    This incubator project from the Apache Foundation demos drinking from the twitter hose with twitter4j and fishing in the streams with Java, Clojure, Python, and Ruby. Very cool and very powerful..

    https://github.com/apache/storm/tree/master/examples/storm-starter

    Testing Storm Topologies in Clojure:

    http://www.pixelmachine.org/2011/12/17/Testing-Storm-Topologies.html

    Vinyasa

    READ this to give your clojure workflow more flow

    https://github.com/zcaudate/vinyasa

    Wrapping up

    Clojure and Java are siblings on the JVM; they should play nicely together. Maven enables them to be easily mixed together in the same project or between projects. For a more indepth example of creating and consuming libraries written in Clojure, see Michael Richards’ article detailing how to use Clojure to implement interfaces defined in Java. He uses a FactoryMethod to abstract the mechanics of getting the implementation back into Java, which make’s the Clojure code virtually invisible from an API perspective. Very nice. Here’s the link:

    http://michaelrkytch.github.io/programming/clojure/interop/2015/05/26/clj-interop-require.html

    Happy hacking!..

    Permalink

    Codex in the REPL

    As I started using coding agents more, development got faster in general. But as usually happens, when some roadblocks disappear, other inconveniences become more visible and annoying. Yeah… this time it was Clojure startup time.

    In one of my bigger projects, the Defold editor, lein test can spend 10 to 30 seconds loading namespaces to run a test that itself takes less than a second. This is fine when you run a full test suite, but painful for agent-driven iteration. And yes, I know about Improving Dev Startup Time, and no, it does not help enough.

    While it would be nice to improve startup time, it would be even better for an agent to actually use the REPL-driven development workflow that Clojure is designed for. Agents won’t naturally do it unless nudged in the right direction. So today I took the time to set it up, and I’m so happy with it that I want to share the experience!

    The Setup

    To make an agent use RDD, it needs to find a REPL, send a form to it, and get a result back. Doing that is surprisingly easy: one convention for discovery, one tiny script, and one skill prompt.

    1. Start a discoverable REPL in project context

    The important part is discoverability. I prefer to use socket REPLs, but I don’t want to pass ports around; the agent should find a running REPL by itself.

    To do that, I added a Leiningen injection in a :user profile that:

    • starts clojure.core.server REPL on a random port
    • writes the port into .repls/pid-{process-pid}.port in the current directory used to start a Leiningen project
    • ensures .repls/.gitignore exists and ignores everything in that directory
    • removes the port file on JVM exit

    This makes it easy to discover the REPLs programmatically while keeping git clean.

    2. Add a script that evaluates forms through that REPL

    To find the port, I vibe-coded an eval.sh script that:

    • looks for port files in .repls to find a running REPL
    • sends one or more Clojure forms to a running REPL using nc
    • prints a friendly message when no REPL is running

    The implementation is trivial, so there is no point in sharing it here. The idea is what matters: find the port in a known location and pipe input forms to the REPL server.

    3. Teach Codex to use it by default for Clojure work

    I added a Codex skill that points to this script and includes common patterns, such as:

    Evaluating in a namespace

    ./eval.sh '(in-ns (quote clojure.string)) (join "," [1 2 3])'
    

    Running tests in-process

    ./eval.sh '(binding [clojure.test/*test-out* *out*] (clojure.test/run-tests (quote test-ns)))'
    

    Some general advice

    Iterate in small steps, etc. You know the drill.

    Findings

    First of all, agentic engineering got noticeably faster. With a warm REPL, Codex can run many small checks while iterating on code.

    But what impressed me more is that it was actually quite capable of using the REPL to iterate in a running VM. For example, when it wanted to check a non-trivial invariant in a function it was working on, it used fuzzing to generate many examples to see whether the implementation worked as expected. That was cool and useful! I don’t typically do that in the REPL myself.

    Turns out Codex can do REPL-driven development quite well!

    Permalink

    Clojure + NumPy Interop: The 2026 Guide to Hybrid Machine Learning Pipelines

    Why choose just one – JVM stability or NumPy’s speed- when it is actually possible to have both?

    Using modern interop tools such as libpython‑clj, developers can integrate Clojure’s machine-learning capabilities with Python’s extensive ecosystem without incurring unnecessary overhead. Teams can now perform numerical computing in Clojure while leveraging the full power of NumPy’s C extensions for vectorization, broadcasting, and linear algebra.

    When teams directly import NumPy arrays into the Clojure workflows, they get the best of both worlds: Clojure’s functional style and concurrency, plus NumPy’s raw performance. For teams developing AI software, this just makes sense. It is a smoother path to scalable, production-ready software solutions- without having to compromise.

    The Interop Landscape

    libpython‑clj: The Gold Standard

    If developers want Clojure and Python to work together, libpython-clj sets the bar. They get direct access to NumPy, SciPy, and Scikit‑learn without extra layers or complications. Thanks to zero-copy memory mapping, data moves between the JVM and CPython without a hitch. Developers won’t waste time converting data in both directions.

    Flexiana has strong expertise in connecting Clojure and Python for machine learning, and several detailed case studies demonstrate how libpython-clj enables teams to use major tools such as NumPy, SciPy, and scikit-learn in production environments. What stands out is how these examples show that developers do not have to choose between Python’s fast research ecosystem and Clojure’s rock-solid stability—they can leverage the strengths of both. This balanced approach helps teams build scalable, production-ready software solutions for real-world projects.

    tech.ml.dataset: Clojure’s Pandas Alternative

    If developers want to handle data directly in Clojure, tech.ml.dataset is the go-to option. It is the closest thing to Pandas on the JVM. The best part? It plugs straight into libpython-clj, allowing data transfer between the JVM and CPython without extra copies. Teams can use Clojure to prepare and manage their datasets before sending them to NumPy for intensive computational tasks.

    Pandas vs. tech.ml.dataset

    FeaturePandas (Python)tech.ml.dataset (Clojure)
    ColumnsFlexible column typesStrongly typed columns
    IndexingLabels and multi‑indexingFunctional style indexing
    Data SharingNeeds serializationZero‑copy with libpython‑clj
    InteropWorks inside Python onlyConnects directly with NumPy

    Neanderthal: Native Clojure Numerics

    Does it not require Python? Neanderthal is a powerhouse for numerical computing in Clojure. It is fast- built on BLAS and LAPACK, and if teams want GPU action, it connects to CUDA and OpenCL. Neanderthal needs direct GPU access without Python; it runs well in the JVM.

    Comparison: NumPy vs. Clojure Native Numerics

    FeatureNumPy (Interop)Neanderthal (Native)
    EcosystemPython ML libraries (SciPy, scikit‑learn, PyTorch)Focused on linear algebra, deep learning, and JVM tools
    PerformanceVery High — native BLAS/LAPACK via C extensions Very High — native BLAS/LAPACK with JVM-native integration (no Python interop)
    Ease of UseFamiliar to Python developersSteeper learning curve for Clojure developers
    MemoryShared via libpython-cljNative JVM/Off‑heap
    GPU SupportCuPy/PyTorch interopBuilt‑in CUDA/OpenCL
    IntegrationWorks best in hybrid workflowsBest for JVM‑only projects
    CommunityLarge Python community, many tutorialsSmaller but focused Clojure community
    DeploymentCommon in research and prototypingStrong fit for production JVM systems
    FlexibilityWide range of ML librariesSpecialized for numerics and performance

    This table shows the trade‑offs clearly:

    NumPy interop is a good choice if teams already work in Python and want access to its machine learning libraries. Neanderthal is better when teams need maximum speed, GPU acceleration, and want to stay fully inside the JVM.

    Setting Up Clojure‑NumPy Environment

    deps.edn Setup

    To begin with, add libpython-clj to the deps.edn file. That is the bridge between Clojure machine learning and Python’s numerical stack.

    {:deps {clj-python/libpython-clj {:mvn/version "2.024"}}}

    Now, double-check where Python is installed on the system. Clojure requires a path to load libraries such as NumPy. Developers need to point to their Python interpreter or virtual environment.

    REPL Integration

    Once developers have configured the dependencies, they can import Python libraries directly into their REPL. 

    (require-python '[numpy :as np])
    
    ;; Simple demo
    (def arr (np/array [1 2 3 4]))
    (np/sum arr)  ;; => 10

    This quick example shows how developers can create a NumPy array and run a few operations, all from Clojure. It’s proof that NumPy interop works smoothly and that AI software development gets the ideal combination: Python’s speed with Clojure’s structure.

    Zero‑Copy Magic

    Here’s where things get really interesting. With tech.v3.dataset, you can move data between the JVM and CPython without making extra copies. This is called zero‑copy integration. 

    • No messing around with serialization, no wasted time.
    • Just prepare your data in Clojure.
    • Transfer it to NumPy for heavy numerical processing, then continue.
    With tech.v3.dataset, you can move data between the JVM and CPython without making extra copies.

    This setup makes Clojure a real contender for numerical computing. Developers are not merely connecting two languages. They are building scalable software solutions that can handle complex, real-world tasks.

    Building a Hybrid ML Pipeline

    Step ❶: Data Preparation

    Use Clojure’s sequence functions for ETL. They make cleaning and shaping data pretty effortless. 

    • Just grab the map, filter, and reduce to process raw data. 
    • When developers need proper tables, add tech.ml.dataset into the combination. 
    • Continue using Clojure for data transformations until the data is ready for heavy numerical computation.  

    Step ❷: Numerical Crunching

    Transfer the ready-made data to NumPy and let Python do the math.

    • Vectorization runs operations across whole arrays.
    • Broadcasting helps when arrays do not match in shape. 
    • Need matrix multiplication or decomposition? NumPy takes care of all the usual linear algebra work.  

    👉 Check out the NumPy official docs for more details.

    Step ❸: Model Integration

    Once the data is ready, load models from Scikit‑learn or PyTorch.

    • Train or load them in Python as needed. 
    • For inference, use libpython‑clj to call Python directly from Clojure.
    • Return results to the JVM for use in production or reporting processes.

    Clojure maintains deployment stability, while Python’s ML ecosystem manages the models.

    Hybrid AI Pipeline

    Why It Matters

    The pipeline uses Clojure and Python, where they work best. Teams get: 

    • Clojure’s organized data processing.
    • Python’s high-powered numerical computing.
    • The JVM’s stability in the backend. 

    Developers can scale their software while leveraging the best features of both languages.

    Benefits of the “Clojure + NumPy” Approach

    Benefits of the “Clojure + NumPy” Approach

    ✔️ REPL‑Driven Experimentation (Try Ideas Instantly)  

    Clojure’s REPL makes coding fast- write, change, and run code on the spot. That loop makes it easy to test ideas. In AI software development, where teams often need to experiment extensively, that speed makes a difference. Sharing snippets and testing together keeps work moving. It is simply a smoother way to work, especially when everyone is collaborating to solve tough problems.

    ✔️ Functional Integrity (Stay Functional and Clean)

    Python’s math libraries often change developers’ data in place, which can lead to unexpected side effects. With Clojure for machine learning, they can integrate NumPy into a functional workflow. Their data remains predictable, functions do not modify the external state, and debugging becomes less painful. They spend less time chasing weird bugs or wondering why their output changed. What is the end result for teams working on numerical computing in Clojure? Code is clean, pipelines are stable, and growth is easier.

    ✔️ Enterprise Scaling with Clojure Concurrency (Scale Up Without Slowing Down)

    On the JVM, Clojure manages heavy workloads with real concurrency. Combine with NumPy interop to speed up numerical computations, and teams get an environment that can handle huge datasets without slowing down. Flexiana has seen real drops in latency when they combine JVM concurrency with NumPy’s speed in their ML pipelines. It is not just about raw speed- this setup lets teams scale up confidently, with the assurance their system won’t fail when the load grows.

    ✔️ Balanced Strengths (Best of Both Worlds)

    Clojure handles concurrency and orchestration. It also handles enterprise tasks. NumPy manages the math work. Together, they produce an accurate and efficient pipeline. Developers get both performance and stability, so they don’t have to choose. If the team needs to manage distributed workloads while handling heavy numeric processing, this approach works well. Tools like libpython-clj1 tie everything together, making integration feel seamless. It is a solid way to build hybrid systems that actually last.

    Common Pitfalls and How to Avoid Them

    Common Pitfalls and How to Avoid Them

    Mismanaging Memory Between JVM and Python

    Data coordination between Clojure and Python is challenging. If teams are not paying attention, they will end up copying large datasets multiple times, wasting memory and slowing everything down.

    How to avoid it: Go for zero-copy integration whenever possible, using tools like libpython-clj1 or tech.ml.dataset. Do as much as teams can in Clojure, and only bring in NumPy when it is really needed for that speed. Always monitor memory usage when dealing with large arrays.

    Overusing Interop Calls (performance hit)

    Interop is great, but there is an overhead involved. When developers repeatedly call Python functions from Clojure in a tight loop- thousands of times- performance drops drastically.

    How to avoid it: Batch the work. Push large chunks of data to NumPy and allow it to process the calculations. Keep the control flow in Clojure and cut down on all those frequent back-and-forth calls.

    Ignoring Concurrency Design

    Clojure runs on the JVM, which indicates it is built for concurrency. But if teams forget to design for it, workloads jam up. Python’s GIL limits running things in parallel on the Python side.

    How to avoid it: To handle concurrent processes, rely on Clojure’s concurrency tools- atoms, refs, agents, and futures. Let Python focus on numerical computation, while Clojure runs the show and scales things up. It helps to avoid running into the GIL’s roadblocks.

    Not Testing Startup/Deployment Properly

    Interop setups tend to fail when a developer moves from the laptops to production- wrong paths, missing dependencies, corrupted environments. On-site equipment might suddenly fail at other locations.

    How to avoid it: Test the startup scripts. Ensure the Python interpreter is configured correctly, with automated CI/CD checks to quickly identify issues.

    Driving Results Through Engineering Excellence

    Engineering Productivity

    Speed of Development  

    Clojure brings everything together smoothly. It consolidates the entire pipeline into one place without creating confusion. Developers can connect Python libraries, JVM tools, and their own logic pretty fast- no mountains of boilerplate, just straight to the real problems. And with the REPL, Developers are not stuck waiting for long builds. Quick adjustments and tests keep projects on track.

    Maintainability  

    Clojure sticks to a functional style, so the code stays clean, and the data flows in a way that actually makes sense. Side effects remain under control. When your pipeline gets bigger, you spot bugs early, and resolving them doesn’t become a hassle. New people can jump in and understand what’s happening without getting confused, which makes onboarding much easier. Bottom line: fewer nasty surprises, easier upkeep.

    Long‑Term Stability  

    The JVM has been around forever, and people trust it. Years of tweaking, monitoring, and deploying mean it just works. NumPy runs efficiently, so systems scale up and handle heavy loads with ease. It remains fast and stable as workloads increase.

    Team Collaboration  

    The REPL makes small changes easy to test. Fast result sharing keeps everyone in sync. Teams scale with ease thanks to clear feedback that shows changes.

    Integration Flexibility  

    Clojure connects easily to Python, Java, and JVM tools. It plays nicely with the enterprise tools that teams already have, and they still get access to Python’s whole ML world. Teams arenot required to choose sides- they can use what works best from both. They get the freedom to bring in new tools without breaking what is already working.

    ❓ Quick Answers to Common Questions

    Performance and Reliability

    Q1: Does libpython‑clj make code slow?  

    Not really. The primary slowdown stems from repeatedly switching between Clojure and Python. For heavy numerical stuff, that extra cost barely matters compared to how fast NumPy runs.

    Q2: Can I use this in production?  

    Absolutely. Real-world teams rely on it for JVM reliability and Python’s ML strength. Test startup and deployment the same way you test other tools.

    Q3: How is memory usage?  

    Both the JVM and Python consume resources. Pay attention to memory, especially if you’re working with huge datasets.

    Q4: Is libpython-clj still maintained?

    Indeed. The Clojure community ensures compatibility with the latest versions of Python and keeps it up to date.

    Concurrency and Scaling

    Q1: What about the Python GIL?  

    Python code still runs under the Global Interpreter Lock. Clojure handles concurrency separately, so workloads scale, whereas Python handles concurrency internally.

    Q2: Does it support parallel workloads?  

    Yes. Clojure gives you concurrency tools like atoms, refs, agents, and futures, all running on the JVM, which is built for scale. Python handles numerical processing.

    Integration and Flexibility

    Q1: Does it support GPU acceleration?  

    While Python libraries such as TensorFlow, PyTorch, and CuPy support GPU acceleration, libpython-clj itself is only an interop layer and neither enables nor restricts GPU usage.

    Q2: Is it compatible with virtual environments?  

    Yes. Set libpython-clj to your Python virtual environment to keep dependencies simple.

    Q3: Can I mix and match multiple Python libraries?  

    Yes. Import and use any Python library you want, just like you would in Python. Clojure ties everything together.

    Developer Experience

    Q1: How difficult is debugging?  

    Quite simple. Errors show up directly in Clojure, and the REPL makes it easy to try code in small steps.

    Q2:Does the REPL help collaboration?  

    Definitely. The REPL makes quick tests easy, and results are simple to share.

    Shaping the Future of Hybrid ML

    Trends in Hybrid ML Pipelines.

    Hybrid ML pipelines are gaining popularity quickly. Teams want the dependable stability from the JVM, but they are not willing to give up Python’s powerhouse ML libraries. So, rather than choosing one, an increasing number of projects use both. Combining them makes it way easier to scale up, keep things running smoothly, and adjust quickly as the workload changes.

    Growing Role of Interop Tools Like libpython‑clj1.

    Interop tools like libpython-clj1 are no longer just for experimentation. These days, libpython-clj1 is the go-to for integrating Clojure and Python in real production code. Developers can import NumPy, SciPy, or Scikit-learn right from Clojure- no awkward workarounds. As more teams join, tools like this are becoming the backbone of hybrid pipelines.

    Potential Improvements in Zero‑Copy Integration.

    Zero-copy integration is already a game-changer. Eliminating data duplication saves time and memory. Looking ahead, there is room to further improve it. Think faster pipelines, better support for huge datasets, smoother GPU acceleration, and handling complicated data structures without the usual headaches. All this will further reduce overhead and make everything feel almost effortless.

    Where Flexiana Sees Hybrid ML Heading in 2026 and Beyond.

    At Flexiana, we have seen hybrid ML pipelines move out of the “experimental” corner and take center stage for big companies. Here is where things are headed by 2026 and beyond:

    • Teams will see hybrid ML everywhere- finance, healthcare, retail, and more.
    • Cloud-native integration will become more integrated.
    • Teams will prioritize maintainability and stability over short-term speed.
    • Hybrid setups will be the default, not just a backup plan.

    The direction is clear: Hybrid ML pipelines are not a passing trend. They are the new normal, enabling developers to leverage the best tools from both worlds and get things done.

    Final Thoughts 

    Clojure machine learning brings the rock-solid reliability of the JVM, while NumPy offers that raw speed Python’s known for to process numerical data. Combine them, and developers get a hybrid ML pipeline that does not force them to choose between stability and performance. With tools like libpython-clj1, moving data between the two just works. Team can access enterprise-level concurrency and fast numerical work at the same time- no compromises, especially if teams are pushing the limits of numerical computing in Clojure.

    Bringing these strengths together enables teams to move faster in AI software development, test new ideas without getting stuck, and keep their codebase clean and scalable as their needs grow. It is a practical setup- flexible, efficient, and ready to handle whatever real-world demands come their way.

    If you are ready to kick off your own hybrid ML pipeline, Flexiana can help you blend JVM reliability with NumPy speed. Let’s get started.

    The post Clojure + NumPy Interop: The 2026 Guide to Hybrid Machine Learning Pipelines appeared first on Flexiana.

    Permalink

    Why Gaiwan Loves the Predictive Power of Universal Conventions


    A one-stop-shop for your entire dev environment: bin/launchpad

    Why Gaiwan Loves the Predictive Power of Universal Conventions

    Close to four years ago we released Launchpad, a tool which has been indispensible in our daily dev workflow ever since. In the latest Clojure Survey, over 85% of respondents indicated "The REPL / Interactive Development" is important for them. We already explained at length in our Why Clojure what exactly we mean when we say "Interactive Development", and why it is so important.

    In order to do Interactive Development, you need a live process to interact with, connected to your editor or IDE. This is where Launchpad comes in. It focuses on doing one thing well: starting that Clojure process for you to interact with, with all the bits and bobs that make for a pleasant and productive environment. It&aposs a simple idea, and we&aposve heard from numerous people how it has made their life easier. But not everyone seems to get it.

    Ovi Stoica has been doing great work on ShipClojure, and as part of that work he created leinpad, a Launchpad for Leiningen users. It&aposs been very validating to see people pick up these same ideas and run with them. Launchpad is for Clojure CLI only, we switched away from Leiningen as soon as the official CLI came out in 2018 and never looked back. Leiningen vs Clojure CLI is perhaps a topic we can dig into in another newsletter, but needless to say it&aposs good to see a Launchpad alternative for people still using Leiningen, by need or by choice.

    The Leinpad release announcement sparked some interesting discussion on Clojurians Slack, going back to some of the Launchpad design decisions. Launchpad strongly recommends that people create a bin/launchpad script, just like we also recommend that people create a bin/kaocha script to invoke the Kaocha test runner. In both cases there are two related reasons why we feel strongly about this. We want both of these to become universal conventions, so that when you start working on a Clojure project for the first time, you can safely assume that bin/kaocha runs your tests, and bin/launchpad starts your development process. In any project you can put something in that location that does that job, regardless of your stack and setup. It&aposs a form of late binding, and it means a new hire doesn&apost need to pore over the README, or worse, ask around, to know how they&aposre supposed to run the tests or run a dev environment.

    Each time people instead decide they prefer bb run launchpad, or clj -X:kaocha, or any other variant, they break the predictive power of that convention. They muddy the water for everyone. This is why we resisted a -X style entrypoint for Kaocha, despite later accepting a community contribution that implements one. A convention is only as powerful as its adoption rate.

    Besides being a convention, it&aposs also a place where you can customize the Launchpad (or Kaocha) behavior. For Launchpad this is especially important because the goal for the bin/launchpad script is to be a one-stop-shop for your entire dev environment. That can mean installing npm packages, running docker containers, loading stub data, anything you need so that a new contributor can arrive, run bin/launchpad, and be productive.

    Recently a team member had trouble running Launchpad because their babashka version was out of date. They reached for the tool they usually use when confronted with "works on my machine" issues: Nix. Nix ensures a reproducible environment, where everyone is using the exact same versions and packages. It&aposs like tools.deps but for system software, and solves some of the issues around Phantom Dependencies.

    This meant replacing the bb shebang with nix-shell. This shows the power of a filesystem convention like bin/launchpad. The facade stays the same, but what&aposs under the covers is now radically different.

    -#!/usr/bin/env bb
    +#!/usr/bin/env nix-shell
    +#! nix-shell -i bb -p babashka
    +#! nix-shell -I nixpkgs=https://github.com/NixOS/nixpkgs/archive/refs/tags/25.11.tar.gz

    Later on I introduced GStreamer to the project, to support multimedia playback. This too is (partially) a system dependency, and some people on the team struggled to get it working. In this case I was able to build on the nix-shell approach, adding the additional dependencies. So instead of adding a section to the README that explains how to either brew or apt install gstreamer, I added the necessary bits to our one-stop-shop. And all the while it&aposs still just bin/launchpad. I&aposd love to see a cultural shift where we no longer accept ten steps of outdated instructions in a README just to get a dev environment. Dev tooling is part of our job, and we do ourselves, our team members, and our employers an injustice by treating it with any less care than we give to the customer facing bits of our software.

    #tea-break

    At Gaiwan we share interesting reads and resources in our #tea-break channel.

    EU Tech Map and European Alternatives: We care about EU&aposs software rebellion to move away from American companies and doing things in-house, supporting OSS, etc. It also sets a benchmark and a path for other countries to follow.

    The job losses are real — but the AI excuse is fake by David Gerard. Layoffs are done for economic reasons, but blamed on AI because that sounds better, and meanwhile the whole situation is used to get people to accept lower job offers. What&aposs your experience with this?

    An AI Agent Published a Hit Piece on Me by Scott Shambaugh. In case you haven&apost seen it already, this is 🤯

    One or more of us is considering this really cool open source Linux device, by an Indian team. And not as expensive as you might have thought. (We are not compensated for writing about this Kickstarter project; we really just think it&aposs cool.) So many devices, so little time...

    Permalink

    The Rest of the Story: February Edition - JVM Weekly vol. 164

    This time we have plenty of material: from a fascinating interview with Kotlin’s creator, through a groundbreaking change in Minecraft, to a whole wave of new projects proving that Java UI is in much better shape than anyone would expect. Let’s dive in!

    Thanks for reading JVM Weekly! Subscribe for free to receive new posts and support my work.

    1. February: The Rest of the Story

    Article content

    Gergely Orosz on his Pragmatic Engineer podcast conducted an extensive interview with Andrey Breslav, the creator of Kotlin and founder of CodeSpeak. The conversation is a goldmine of little-known facts about Kotlin’s history - for example, that the first version of Kotlin wasn’t a compiler but an IDE plugin, that the initial team consisted mainly of fresh graduates, or that smart casts were inspired by the obscure language Gosu.

    But the most interesting thread concerns the future. Breslav is now building CodeSpeak - a new programming language designed to reduce boilerplate by replacing trivial code with concise natural language descriptions. The motivation? Keeping humans in control of the software development cycle in the age of LLM agents. As he put it - in the future, engineers will still be building complex systems, and it’s worth remembering that, even if Twitter is trying to convince us otherwise (heh).


    Moderne announced that their OpenRewrite platform - known primarily in the Java ecosystem as a tool for automatic code refactoring - officially now supports Python. Python code can now be parsed, analyzed, and transformed alongside Java and JavaScript within the same Lossless Semantic Tree (LST).

    Article content

    Why does this matter for us JVM folks? Because modern systems rarely evolve in isolation. A Java service might expose an API consumed by a Python integration, a shared dependency might appear in backend services, frontend tooling, and automation scripts. With Python in the LST, dependency upgrades can be coordinated in a single campaign across multiple languages.

    An interesting extension to the mission of one of the JVM ecosystem’s most important tools.


    Article content

    Igor Souza wrote a fun article celebrating the 40th anniversary of Legend of Zelda, comparing the triangle of performance improvements in Java 25 to the legendary Triforce. The analogy connects three key improvements: optimized class caching (AOT caching) for faster startup, Compact Object Headers for more efficient memory use, and garbage collection improvements. Java “Power, Wisdom, Courage” - genuinely a lovely connection with gaming lore.

    Article content
    Maybe not my favourite series, but I had a lot of fun with some of the editon. Happy Birthday, Link!

    Continuing the video games related topics, big news from the gamedev side of JVM - Mojang announced that Minecraft Java Edition is switching from OpenGL to Vulkan as part of the upcoming “Vibrant Visuals” update. This is a massive change for one of the most popular games written in Java. The goal is both visual improvements and better performance. Mojang confirmed that the game will still support macOS and Linux (on macOS through a translation layer, since Apple doesn’t natively support Vulkan).

    Interestingly, this is probably the first Java game to use Vulkan. Modders should start preparing for the migration - moving away from OpenGL will require more effort than a typical update. Snapshots with Vulkan alongside OpenGL are expected “sometime in summer,” with the ability to switch between them until things stabilize.

    Article content

    PS: I started to introduce my daughter to Minecraft ❤️ Fun time.


    Article content

    JetBrains announced that starting from version 2026.1 EAP, IntelliJ-based IDEs will run natively on Wayland by default. This is a significant step, particularly since Wayland has become the default display server in most modern Linux distributions.

    One small difference in practice - the startup splash screen won’t be displayed, because it can’t be reliably centered on Wayland.

    Positioning things is one of the hardest things in IT.

    Article content

    Bruno Borges published a practical guide on configuring JDK 25 for GitHub Copilot Coding Agent - the ephemeral GitHub Actions environment where the agent builds code and runs tests. By default, the agent uses the pre-installed Java version on the runner, which can lead to failed builds if the project requires newer features. The solution? A dedicated copilot-setup-steps workflow with actions/setup-java.

    Short, concrete, and useful if you’re starting to experiment with coding agents in Java projects.


    SkillsJars is a new project showcased on the Coffee + Software livestream (Josh Long, DaShaun Carter, James Ward ) - a registry and distribution platform for Agent Skills via... Maven Central.

    The idea is simple and simultaneously crazy in the best possible way: Agent Skills (the SKILL.md format introduced by Anthropic for Claude Code, also adopted by OpenAI Codex) are modular packages of instructions, scripts, and resources that extend AI agent capabilities - e.g., a skill for creating .docx documents, debugging via JDB, or building MCP servers. SkillsJars packages these skills as JAR artifacts and publishes them on Maven Central under the com.skillsjars group, with full support for Maven, Gradle, and sbt.

    Article content

    The catalog already includes a Spring Boot 4.x skill (best practices, project structure, configuration), an agentic JDB debugger by Bruno Borges, a browser automation skill (browser-use), and official Anthropic skills for creating presentations, PDFs, frontend design, and building MCP servers. Anyone can publish their own skill - just point it to a GitHub repo.

    Maven Central as a package manager for agentic AI - we truly live in interesting times.


    Johannes Bechberger from the SapMachine team at SAP created a fun quiz where you have to guess the minimum Java version required from a code snippet. Over 30 years, Java added generics, lambdas, pattern matching, records... and it turns out most of us can’t precisely recall which feature arrived in which version.

    A perfect way to kill five minutes over coffee (or an hour, if you’re as nerdy as I am).

    Article content

    If you think it’s too easy... I dare you to try Java Alpha version with alpha features 😉


    Akamas published an analysis of the state of Java on Kubernetes based on thousands of JVMs in production.

    The findings? Despite it being 2026, most Java workloads on K8s run with default settings that actively hurt performance. 60% of JVMs have no Garbage Collector configured, most heap settings are at defaults, and a significant portion of pods run with less than 1 CPU or less than 1 GiB RAM - which is a serious bottleneck for Java’s multi-threaded architecture. An old problem, but the data is striking.

    PS: Ergonomic Profiles was always a great Idea IMHO.


    JetBrains released an extension for Visual Studio Code that enables converting Java files to Kotlin. The converter (J2K) uses the same engine as IntelliJ IDEA. Just open a .java file, right-click, and select “Convert to Kotlin.”

    An interesting move - JetBrains clearly wants the Kotlin ecosystem to expand beyond their own IDE.


    Robin Tegg, whose piece on Java UI landed in the newsletter two weeks ago, created an awesome page - a comprehensive guide to Java UI frameworks, from desktop (JavaFX, Swing, Compose Desktop) through web (Vaadin, HTMX, Thymeleaf) to terminal (TamboUI, JLine, Lanterna). The motivation? Frustration with outdated articles referencing dead libraries. The result is the best single source of knowledge on the current state of Java UI in 2026.

    Article content

    If anyone tells you “you can’t do UI in Java” - send them this link.


    Scala Survey 2026 - VirtusLab and the Scala Center have launched their annual community survey, and if you’re using Scala in any capacity, your 5 minutes can directly shape the language’s roadmap, library ecosystem, and tooling priorities. The survey evaluates Scala adoption patterns, pain points, and what the community actually needs - and the results have historically influenced real decisions about where development effort goes.

    Take the survey here. Whether you’re a daily Scala developer or someone who occasionally dips into the ecosystem, your perspective matters. That’s unique possibility to shape the language

    Article content

    To wrap this section - 100 most-watched presentations from Java conferences in 2025 is a solid list to catch up on. And the article on 10 modern Java features that let you write 50% less code is a good refresher, especially for those who are mentally stuck on Java 8.

    2. Release Radar

    Article content

    Quarkus 3.31

    Quarkus 3.31 is a major release that arrived after a two-month gap since the last feature release. The headline addition is full Java 25 support, but the changelog is genuinely impressive. New Panache Next - the next generation of the Panache layer with improved developer experience for Hibernate ORM and Hibernate Reactive. Upgrade to Hibernate ORM 7.2 and Reactive 3.2. Support for Hibernate Spatial. Upgrade to Testcontainers 2 and JUnit 6 (yes, JUnit 6!). New quarkus Maven packaging with a dedicated lifecycle. On top of that: security annotations on Jakarta Data repositories, OIDC token encryption, OAuth 2.0 Pushed Authorization Requests, headless AWT on Windows for native images, and much more. Requires Maven 3.9.0+.

    Release Notes

    Eclipse GlassFish 8.0

    GlassFish 8, released February 5th, is the first production-ready implementation of the full Jakarta EE 11 platform. The release, led by the OmniFish team, brings support for virtual threads in HTTP thread pools and managed executors, an implementation of Jakarta Data (repositories for working with both JPA entities and Jakarta NoSQL entities), and a new version of Jakarta Security with more flexible authentication options. Integration with MicroProfile 7.1 (Config, JWT, REST Client, Health). Requires JDK 21 as a minimum, supports JDK 25. Led by Arjan Tijms, Ondro Mihályi, and David Matějček, GlassFish is returning as a serious production option.

    Release Notes

    Open Liberty 26.0.0.1

    Open Liberty 26.0.0.1 is the first release of IBM’s application server in the new year - a transition from the 25.x branch to 26.x. The main addition is log throttling - a mechanism for automatically suppressing repeated log messages that simply didn’t exist in Liberty before. Throttling is enabled by default: Liberty tracks each messageID using a sliding window and after 1000 repetitions within five minutes begins suppressing messages, logging an appropriate warning. Throttling can be configured at the messageID or full message level (throttleType), and limits can be changed (throttleMaxMessagesPerWindow) - or disabled entirely by setting the limit to 0.

    Security fixes include a patch for XSS (CVE-2025-12635, CVSS 5.4), issues with wlp password key decoding, and a NullPointerException in SocketRWChannelSelector. Two new guides on observability with OpenTelemetry and Grafana were also added.

    Worth noting: the beta is developing a Model Context Protocol Server feature (mcpServer-1.0) - allowing Liberty application business logic to be exposed as tools for AI agents. Beta 26.0.0.2 has already added role-based authorization and async support. MCP in a Jakarta EE application server - now that’s an interesting combination.

    Release Notes

    BoxLang 1.9.0

    BoxLang 1.9.0 announced as “production-ready” - a release focused on stability, with over 50 bug fixes, improved datasource lifecycle management (eliminating connection leaks), and better context management (eliminating memory leaks).

    But what exactly is BoxLang? It’s a modern, dynamically typed JVM language from Ortus Solutions — a company well known in the ColdFusion world for the ColdBox framework and CommandBox tooling. BoxLang is essentially an answer to the question “what if we took the best ideas from CFML, Python, Kotlin, and Clojure, and built it from scratch on a modern JVM?” The language is 100% interoperable with Java, uses invokedynamic for performance, and its runtime weighs just ~6 MB. Interestingly, BoxLang has a dual parser — it can natively run existing ColdFusion/CFML code without modification, which is critical for migrations from Adobe ColdFusion or Lucee (and is also Ortus’s primary business model).

    The project’s ambitions are broad — BoxLang targets multi-runtime deployment: from classic web servers, through AWS Lambda, to iOS, Android, and WebAssembly. The project is open source (Apache 2.0), with commercial BoxLang+ and BoxLang++ plans for enterprise support. Ortus also announced a cloud-native version at $5/month — a clear signal they’re trying to move beyond the ColdFusion niche and compete more broadly in the dynamic JVM language space.

    Release Notes

    Apache NetBeans 29

    Apache NetBeans 29 - released February 23rd, so completely fresh. Under the hood there’s plenty of substance: Gradle Tooling API upgraded to 9.3.0, bundled Maven updated to 3.9.12, Ant bumped to 1.10.15. On the Java side — performance fixes for Find Usages and refactoring, better support for import module (keyword highlighting), fixed form designer.

    Notably, NetBeans is already being tested on JDK 26-ea and sets --sun-misc-unsafe-memory-access=warn (JEP 498) — clearly laying the groundwork for upcoming changes to sun.misc.Unsafe access. The IDE supports running on JDK 25, 21, or 17, with preliminary support for JDK 26. Refactoring problem propagation to LSP clients has also been improved — NetBeans continues developing its role as a Language Server backend, not just a classic IDE.

    Release Notes

    Ktor 3.4.0

    Ktor 3.4.0 - a new version of the Kotlin framework for building asynchronous HTTP servers and clients. A stability-focused release, but with several noteworthy additions. Most important: OpenAPI documentation generation from code — a new compiler plugin combined with the describe API lets you build a Swagger model dynamically, directly from the routing tree. No more manually maintaining static OpenAPI files that drift out of sync with the code.

    Also added: Zstd compression support (Facebook’s algorithm offering an excellent compression-to-speed ratio) in the new ktor-server-compression-zstd module, duplex streaming for the OkHttp engine (simultaneous sending and receiving over HTTP/2), and a new HttpRequestLifecycle plugin - allowing in-flight requests to be automatically cancelled when a client disconnects. The last one is a nod to structured concurrency: a client disconnection cascades to cancel the coroutine handling the request along with all launch/async children, making resource management for long-running operations considerably cleaner. Currently works with Netty and CIO engines.

    Release Notes


    3. Github All-Stars

    Article content

    Krema - Tauri, but in Java

    Krema is probably the most interesting new project in this edition. For those who know Tauri from the Rust ecosystem - Krema is its Java equivalent. Lightweight, native desktop applications using the system webview instead of a bundled Chromium. Backend in Java, frontend in React/Vue/Angular/Svelte, communication through a type-safe IPC bridge.

    Article content

    Key features: system webview (WebKit on macOS, WebView2 on Windows, WebKitGTK on Linux); native communication via Project Panama (Foreign Function & Memory API from Java 25) - no JNI, annotate Java methods with @KremaCommand and call them from the frontend with full type safety, plugin system (SQLite, WebSocket, file upload, window positioning, autostart), native packaging with GraalVM or as a JAR.

    The project looks well thought-out, and if it develops, it could be a game-changer for desktop apps in Java.

    TamboUI - Terminal UI for Java, finally taken seriously

    TamboUI announced by Cédric Champeau (known from Gradle and Micronaut) and Max Rydahl Andersen (JBang, Red Hat ) - a modern Terminal UI framework for Java. It was born from the observation that Rust has Ratatui, Go has Bubbletea, Python has Textual - and Java? System.out.println and prayers.

    Article content

    TamboUI offers a multi-level API: from low-level widget primitives (like Ratatui), through a managed TUI with event handling, up to a declarative Toolkit DSL that handles the event loop and rendering thread for you. Immediate-mode rendering, constraint-based layout, CSS support, PicoCLI integration, multiple backends (JLine, Aesh, Panama).

    And most importantly - full GraalVM native image compatibility, making Java a serious player in terminal tooling with low memory usage and fast startup. Core works on Java 8+, but it’s most enjoyable with modern Java idioms.

    JADEx - Null Safety for Java Without Changing the Language

    JADEx (Java Annotation-Driven Extensions) is an approach to null safety in Java through annotations and compile-time processing - without waiting for Valhalla or a language spec change. The discussion on r/java shows the topic is still very much alive, with demand for pragmatic solutions.

    JOpus - Wrapper for the Opus Codec

    JOpus is a high-performance Java wrapper for the Opus audio codec. Opus (not the one you might be thinking of) is an open audio format that excels at VoIP, streaming, and gaming - and is now easily accessible from the JVM.

    Article content

    ChartX - OpenGL Charting Library

    Article content

    ChartX is a library for creating hardware-accelerated charts using OpenGL. An interesting technology choice given that Minecraft is moving to Vulkan - but if you need performant, hardware-accelerated visualizations in Java, worth a look.

    JBundle - Packaging JVM Applications

    JBundle is a tool for packaging JVM applications into distribution-ready formats. In the era of jpackage and GraalVM native image, JBundle offers yet another option in the deployment tooling ecosystem.


    As a wrap up, I have the invitation 😁

    Article content

    PS: The full KotlinConf’26 schedule is live - it’s officially time to start planning your conference experience! This year’s program highlights the ideas, tools, and real-world practices shaping the future of Kotlin. Over two days, you’ll hear from the Kotlin team and community experts as they share insights, experiments, and lessons learned from building at scale.

    Amon speakers, you will find:

    The full list you can find there.

    Thanks for reading JVM Weekly! Subscribe for free to receive new posts and support my work.

    Permalink

    The YAML Trap: Escaping Greenspun’s Tenth Rule with BigConfig

    Greenspun’s Tenth Rule is a famous (and delightfully cynical) adage in computer science. While it was born in the era of C and Fortran, it has never been more relevant than it is today in the world of Platform Engineering.

    If you’ve ever felt like your CI/CD pipeline is held together by duct tape, YAML-indentation prayers, and sheer willpower, you’ve lived this rule.

    What is Greenspun’s Tenth Rule?

    In the early 90s, Philip Greenspun stated:

    “Any sufficiently complicated C or Fortran program contains an ad-hoc, informally-specified, bug-ridden, slow implementation of half of Common Lisp.”

    The core insight is that once a system reaches a certain level of complexity, it inevitably requires high-level abstraction, automation, and dynamic logic. Instead of starting with a powerful, established language (like Lisp) built for those tasks, developers often “accidentally” reinvent a mediocre version of one using brittle configuration files and makeshift scripts.

    The Rule in the DevOps Ecosystem

    In DevOps, we strive for Infrastructure as Code (IaC). However, because we started with static configuration formats (YAML/JSON) and tried to force them to perform complex logic, we’ve essentially proven Greenspun right.

    1. The YAML “Programming” Trap

    Tools like Terraform, Ansible, Helm, and GitHub Actions began as simple configuration formats. But as users demanded loops, conditionals, and variables, these tools evolved into “accidental” languages.

    • The Problem: You end up writing complex business logic inside strings within a YAML file.
    • The Reality: You are using a “bug-ridden implementation” of a real programming language, but without the benefit of a debugger, a compiler, or proper unit testing.

    2. Kubernetes as a Distributed Lisp

    Some architects argue that Kubernetes is the ultimate manifestation of this rule. Its control loop the constant cycle of reconciling desired state vs. actual state mimics the recursive nature of Lisp environments. It is, in essence, a programmable platform designed to manage other programs.

    Escaping the Trap: Putting Lisp Back in Ops

    The industry has invested massive human capital into building Ansible roles, Helm charts, and Terraform modules. We shouldn’t throw them away, but we must stop trying to make them do things they weren’t designed for.

    How do we escape Greenspun’s trap without rebuilding everything from scratch? By assimilating these tools (to borrow a 90s Star Trek reference).

    This is the core design principle of BigConfig. Instead of fighting against limited YAML DSLs, BigConfig uses Clojure a modern, production-grade Lisp to wrap and orchestrate existing tools.

    The BigConfig Philosophy: Express infrastructure logic with the most powerful dynamic language available, while still leveraging the ecosystem you already have.

    Why it Matters: The Power of Assimilation

    The Tenth Rule is a warning: Don’t reinvent the wheel poorly. If your infrastructure requires complex logic, stop forcing it into a flat config file.

    From Static Files to Fractal Architecture

    While a standard Helm package is limited strictly to Kubernetes, a BigConfig package is a Clojure function. Because BigConfig assimilates Ansible and Terraform alongside Helm, it isn’t siloed.

    • Truly Cloud Native: A Kubernetes application that requires specific cloud resources (like an S3 bucket or an RDS instance) can be abstracted into a single, cohesive unit.
    • First-Class Functions: In BigConfig, everything is a function. This leads to a fractal architecture where every layer from a single container to a multi-region cloud deployment is governed by the same recursive logic: Observe, Diff, and Act.

    Ready to stop writing logic in YAML?

    Operations is a hard problem. YAML is too rigid, and Go is too low-level for rapid infrastructure iteration. While Python and JavaScript are popular, they lack the REPL-driven development flow that makes infrastructure-as-code feel truly interactive.

    Clojure is the most robust Lisp available today and it won’t let you down.

    Conclusion

    Greenspun’s Tenth Rule isn’t just a witty observation; it’s a technical debt warning. When we try to solve 21st-century infrastructure challenges using static configuration files, we inevitably end up building “shadow” programming languages that are difficult to test, impossible to debug, and fragile to scale.

    By embracing a functional, Lisp-based approach through BigConfig, we stop fighting the limitations of YAML and start leveraging the power of actual logic. Instead of building a “bug-ridden implementation of half of Common Lisp,” we use the real thing Clojure to orchestrate, automate, and scale.

    The goal of Platform Engineering shouldn’t be to write more scripts; it should be to create elegant, recursive systems that can manage themselves. It’s time to move past the duct tape and prayers and give our infrastructure the robust, dynamic foundation it deserves.

    Would you like to have a follow-up on this topic? What are your thoughts? I’d love to hear your experiences.

    Permalink

    Managing Complexity with Mycelium

    Software architecture is at root a creature of human frailty. The sacred cow of clean code and the holy grail of design patterns are understood to be, at least in practical terms, little more than tricks to help people keep their sanity along the way. Human cognitive capacity is strictly limited, and we're still figuring out ways to reliably build machines significantly more complex than can be held in a single mind. Current attempts to offload coding tasks to language models are hitting the same wall. These models can be brilliant, but only up to a point. They’ll effortlessly compose a flawless function, but when challenged to manage a project with a thousand such moving parts, they quickly lose the plot. This problem is commonly known as context rot, but it might equally well be called a coding architecture failure.

    If one hands an LLM a big pile of mutable state and loosely defined relationships, a solution that doesn’t actually work will be hallucinated inevitably, and when it does work, it will do so largely by accident. This comes about because there are just too many moving parts, and the ground truth cannot be kept track of. Humans are known to have exactly the same problem. Enormous amounts of time are spent chasing down bugs that exist because some distant part of the app decided to tweak a variable it didn’t own. Shared mutable state quickly leads to an overwhelming information flow, generating invisible threads between components that make it nearly impossible to know the scope of the change being made. Seemingly innocuous code changes end up mutating state in unintended ways that lead to unexpected consequences.

    We solve difficult problems by dividing them into smaller, more manageable ones, and then composing them together. Successful architecture requires separable components, each with a function that is easy to grasp, and creating interfaces between them to abstract over their internal details and present to the outside world only their functional aspects. We must discover the boundaries between components, separate external effects from internal implementation details, and arrange for each component to control its own context. Then, and only then, can we be sure that we will always be working in a clear context that we can fit in our heads.

    We naturally long for layers of organization. Hierarchies permit us to construct separate, self-contained units which can then be connected together to make larger structures. It's a powerful kind of architecture, one that facilitates writing large projects by abstracting over the complexity of the constituent parts. In working on a given component, we have to know all its details. But in using it, we only need to know what it does, which is entirely reflected in its API surface. The internal complexity is encapsulated within the API boundary. These are building blocks that give us a stable base upon which to build higher-level abstractions. Several such components can be assembled into a bigger block, where the connections between subcomponents become its internal complexity. The composite component becomes a new layer, providing its own API, which can be used by still higher-level abstractions. If what I'm describing sounds familiar, it is because that's exactly the way in which software libraries work. A library is simply a model of a class of problems, and when we encounter these types of problems, we can use it as an off-the-shelf building block.

    A complex system has to be resilient and adaptable. But if every component is hooked directly into every other, there’s no way you can anticipate what a change in any one place will do. There are no boundaries to stop the chaos. Anything that works at scale relies on stable subassemblies. Herbert Simon described it long ago in a parable of two watchmakers. The first built his watches one piece at a time. He had to keep the entire complexity of the watch in his mind as he did his work, for any interruption would undo his progress and send the component pieces clattering to the floor. He worked on a flat plane of complexity, having to think through the workings of the entire watch just to insert a single gear. The other watchmaker did well because he first constructed small, stable modules which he was able to click together. So if he was interrupted, only a small amount of work was lost. He never had to think through more than a few components at a time. The system was resilient because it was built as a hierarchy of subunits, each able to stand on its own merit, and so the watchmaker had to consider a small context rather than the stupefying complexity of the whole.

    Practically any large system can so be divided into smaller, nested subsystems, which communicate with one another as they go about their business. The intimate workings of other subsystems need hardly be known, which permits these modules to form. Individual components are not encumbered by what’s going on at other places where events are transpiring at different paces and according to a different set of circumstances. This is how hierarchies help in managing the complexity of a system. Each subsystem can evolve on its own. A malfunction in one region can be quarantined; it need not bring down the entire enterprise. A useful way to think of hierarchies is to treat them as connective tissue between the various subsystems of the program, as a principal means of control, providing the architecture and the infrastructure that coordinates the internal functioning of the individual parts of the system.

    In contrast, in software development, practitioners are often confronted by the opposite scenario. Large software projects degenerate into a tangled web in which every function depends on a global state or a shared database connection. Humans have difficulty working with such systems because they can’t reason about their pieces independently. If a feature requires more information than can be kept in one’s head at once, guesses and assumptions begin to proliferate.

    Just as humans need clear boundaries to maintain sanity, coding agents are even more vulnerable to cognitive saturation. There’s no intuition in a large language model that can tune out the noise. Everything is placed into its context window. If a piece of code is a labyrinth of obscure dependencies, the agent has to parse through it all to make a single modification. Eventually, the context becomes so cluttered that the purpose of the task gets lost in the entropy. The model starts hallucinating because of its inability to distinguish between the logic it’s supposed to refactor and the three hundred other things it’s presented with.

    Where We Are At

    To understand how we might solve this for both humans and machines, let's first examine the tools we've already developed for managing complexity. The software industry has spent decades working out how to decompose code into manageable pieces. A very powerful set of tools for this purpose is already available. At the inter-process scale, there are microservices, which provide a physical boundary between programs. And at the intra-program level, there are techniques such as message passing, pure functions, and immutable data.

    The functional style is particularly good at taming global state. Here, functions are the smallest building blocks, and pure functions can be considered on their own. The idea here is to build systems from separate, single-purpose parts, by using isolation, composition, and clear contracts inside the application's own logic. Code becomes a pipeline focused on the data flow. A program takes one piece of data as input, and produces another as output, pumping it through a sequence of pure functions. Since data is immutable, it is transparent and inert, having no hidden states or secret side effects. This is how stable contracts are formed. Once the transformations have been specified, the interface is a contract you can trust.

    When you design applications in this way, the overall architecture looks like a network of railway lines, with the input data package needing to get from point A to point B. The package might pass through many different stations on the way to its destination, each one inspecting the package to decide where to route it next. An HTTP handler takes the payload, parses the request, determines the content type, and forwards it on toward an authentication handler. The authentication handler might inspect permissions in payload metadata to determine where to send it next, and so on. Eventually, the package arrives at its intended destination where the data gets serialized, stored, or presented to the user.

    But even with all these great functional tools, we still tend to tangle two rather different kinds of code together. We mix code that cares what the data means and the code that cares how it travels from one component to another. Traditional software design structures embed the routing implicitly in the function call graph. For example, our authentication handler will have the logic to select the next function to invoke within its implementation. The control logic and its internal implementation details, thus, end up being intertwined in an ad hoc manner, resulting in a significant coupling problem. If you wish to alter the flow of your application, you must sift through voluminous amounts of incidental code describing the internal minutiae of component implementations.

    An effective solution to this problem is to use inversion of control by removing routing logic from the functions and elevating it to first-class citizenship in the design. Why is a state machine the natural fit for this, rather than an event bus or dependency injection? Event buses scatter routing logic to the winds with components shouting into the void, making the overall flow impossible to trace. Implicit callbacks hide the flow inside the implementation. State machines, on the other hand, make routing declarative and visible in one place. They force the separation of what to do from how to do it.

    Introducing Mycelium

    I’ve spent a great deal of time considering how to construct a system where distinct components are clearly separated by design, with clear boundaries between semantics of the code and the implementation details. This is the basic conceptual orientation of Mycelium, which treats the program as a recursive ecosystem of workflows, solving the very routing problem described above.

    Clojure provides the tools for writing pure functions, but falls short of giving us guidance on how to orchestrate them when writing large applications. I initially developed Maestro to provide a clear organizational framework, separating side-effectful concerns from pure calculation, and structuring workflows as graphs where the nodes represent computations of state, and the edges represent transitions between them. The nodes are distinct, context-free blocks, responsible for specific tasks, linked by a thin coordinating layer controlling the flow of data between them. The state itself is represented as a map that's passed from one node to another.

    The business logic for each node lives inside a Mycelium component called a cell. Since cells are completely unaware of one another, they are inherently isolated. Each one can be viewed as a miniature self-contained application. It knows how to do its specific job and adheres to a strict lifecycle. It takes a state map, loads the data, runs the logic, and computes a new state as its output. All they can access are the IO resources, and a map containing the input state.

    Maestro is responsible for arranging these transitions, so that the decisions are pure, and their effects are encapsulated. When a component needs to move itself from one state to another, it does not simply reach in and take what it needs. Instead, it updates the state map, and delegates to Maestro to orchestrate the transition. Again, this keeps the code responsible for deciding what will happen next separate and distinct from the private parts of the individual cells.

    Each cell is additionally wrapped in a Malli schema, which gives the cell a protocol to abide by. You can’t simply hope that the LLM will understand your intentions when they’re expressed in plain English. What you need is a formal contract to determine whether the implementation is correct. Malli enables us to specify precisely what a cell is entitled to receive and what it can produce as its output. It's a flexible way to encode deep, structural invariants representing the interface of the cell.

    An agent tasked with constructing a handler for a particular node operates within the constraints of a contract, enforced by the schema, both during development and at runtime. Crucially, the agent doesn't need to scour the codebase to discover the relevant cell; the orchestration layer (acting as a Conductor) assigns the specific cell ID and its schema directly to the agent. It operates within a tightly bounded context provided to it. If the code produced by the agent does not adhere to the contract, if the output is even slightly off, the system will reject it providing meaningful feedback on what went wrong.

    Think, for a minute, about what this does for the scope creep problem. The schema defines the boundary of the cell letting the agent know exactly what keys are in the map, what the data types are, and what the constraints are. Since the components do not interact directly, the agent has a well-defined, perfectly sized context that it needs to understand.

    We now have a self-correcting loop. The primary agent, the Conductor, designs the workflow in EDN. A fleet of smaller, specialized agents do the individual tasks. If one of them makes an error, it is detected by the Malli contract before it can propagate forward to the next node in the graph. The specific validation failure is known at the point where it arose, and its scope is limited to the node that produced it.

    The State Machine Graph as a Contract

    Treating an application's high-level behavior as a state machine graph provides us with a master blueprint. It allows us to determine what the intent of a particular workflow is by reading a declarative schema describing the states and the transitions between the cells. A human can review and approve a data flow diagram, which specifies the semantics of each cell, and the rules guiding the flow of data across them. The details of how each cell functions are abstracted behind its API, and managed by the agent responsible for implementing its functionality. Hence, the orchestrator only needs to concern itself with the routing aspect of the application and ensuring that the schemas of nodes sharing an edge are compatible.

    The orchestration layer is in charge of directing the work, and executing the branching logic. Its sole concern is to examine the results from each node to decide on the next branch to take according to the EDN specification.

    Because the intercellular connections form a directed graph, and since motion is governed by payload state, with the routing logic separated from the cell code, you can, in principle, determine the entire decision tree of the application just by examining the EDN spec. Instead of having to dig through conditional branches buried in thousands of lines of implementation code, you have a declarative map of possibilities.

    How This Works in Practice

    You can glimpse the way in which Mycelium binds these ideas together by examining a snippet from the user-onboarding demo. The workflow definition is the point of departure. We start by defining a cell and its strict contract:

    ;; A cell contract for session validation
    {:id       :auth/validate-session
       :doc      "Check credentials against the session store"
       :schema   {:input  [:map
                            [:user-id :string]
                            [:auth-token :string]]
                  :output {:authorized   [:map
                                           [:session-valid :boolean]
                                           [:user-id :string]]
                           :unauthorized [:map
                                           [:session-valid :boolean]
                                           [:error-type :keyword]
                                           [:error-message :string]]}}
       :requires [:db]}
    
    

    This map represents a stable building block. There is no ambiguity in a declarative specification. A cell is defined by the shape of its input and output, along with its resource requirements. The routing logic is extracted entirely into separate :edges and :dispatches keys in the workflow.

     :edges
     {:validate-session {:authorized   :fetch-profile
                         :unauthorized :error}}
    
     :dispatches
     {:validate-session [[:authorized   (fn [data] (:session-valid data))]
                         [:unauthorized (fn [data] (not (:session-valid data)))]]}
    
    

    The edges shown in the snippet represent the possible transitions. The dispatches constitute a list of node identifiers, each with an associated decision function that examines the state and determines whether it should be processed by the identifier in question. Dispatches are processed on a first come, first served basis; that is, the state will be routed to the first matching identifier found.

    Next, we have the cell associated with the spec, which is responsible for performing the actual work. Following Integrant philosophy, the cells are defined as a collection of multimethods.

    (defmethod cell/cell-spec :user/fetch-profile [_]
      {:id      :user/fetch-profile
       :doc     "Fetch user profile from database"
       :handler (fn [{:keys [db]} data]
                  (if-let [user (db/get-user db (:user-id data))]                
                    (assoc data :profile (select-keys user [:name :email]))
                    (assoc data
                           :error-type    :not-found
                           :error-message (str "User not found: " (:user-id data)))))})
    
    

    Note how the :user/fetch-profile handler doesn’t need to know where the data came from, nor does it decide where to send it. All it receives is the current state. The cell does its work and then returns an updated map. The orchestration layer evaluates the dispatches against this new data to select the next edge.

    Long before a user ever makes a request, the workflow goes through a rigorous validation phase at compile time. During startup, the engine verifies that every cell exists in the registry, that every transition has a valid destination, and that all the input and output schemas chain together with no discontinuities. This is the moment where the blueprint becomes an active, executable process.

    When a request (like POST /api/onboarding) actually arrives, the HTTP routing library recognizes the endpoint and shoves it onward to an onboarding handler. This handler summons the pre-compiled workflow engine, giving it the database connection and the raw request. As the state machine proceeds through its transitions, the Malli schemas are serving as sentinels.

    Reliability, Debugging, and Testing

    The State Map acts as the single source of truth throughout this lifecycle. Every transformation is explicit in the return value, and there are no side channels modifying the state. Like a messenger, it travels through the system, carrying on its person all the data that has been gathered up to this point, as well as associated metadata.

    Because the state map keeps a :mycelium/trace of every transition, you get unparalleled observability. If a workflow fails, you don’t just get a stack trace telling you where in the codebase the crash happened; you get the full history at the moment of failure. You can see the inputs, the previous steps, and the exact data that caused the routing logic to stumble. For the coding agent, it’s as if there’s a black box flight recorder on every single run.

    Such level of observability fundamentally transforms how we test applications. Testing in often seen as a thoroughly distasteful chore, so much so that you will often find people spending more time with mocks and dependency injection than actually writing tests.

    Mycelium treats each fragment of logic as a pure update of a data structure. Testing reduces to a straightforward exercise in data juggling. You don’t have to mock up a database to test how a particular system handles a User Not Found scenario; you simply feed the component a state map lacking the :user key and see what output it produces.

    Because every workflow node is contractually bound by its Malli schema, you can take the :validate-user-data handler, feed it a map of bad data, and check that it sets an :invalid key on the state map. You’re not testing the whole onboarding flow; you’re testing one specific cell.

    In Mycelium, logical integration tests can be performed trivially, simply by executing the workflow with a mock resource map. Resources like the database are passed in separately, so a real Postgres connection can be exchanged for a mock in the test suite. The difference is entirely irrelevant to the workflow.

    ;; A logical integration test
    (deftest onboarding-workflow-test
      (let [ds       (create-test-db!)
            compiled (wf/compile-workflow onboarding-manifest)
            result   (fsm/run compiled
                             {:db ds}
                             {:data {:http-request
                                     {:body {"email" "test@example.com"}}}})]
        (is (some? (:profile result)))
        (is (= "test@example.com" (get-in result [:profile :email])))))
    
    

    Because of the trace history, your test assertions become incredibly descriptive. You aren't just checking if the final result is 200 OK. You are able to check that the system moved from :start to :validate-session to :fetch-profile in the exact order you expected. You get the confidence of a full system test by simply passing mock resources to your workflow, and verifying that transitions happen in correct order.

    Layered Abstraction and Infinite Scale

    The entire design is naturally recursive. A complete system implemented as a network of cells can itself be viewed as a single cell, which offers up an interface and can then be slotted into a yet-larger state machine network. You might have a simple network handling user login, which becomes a component in a medium-scale network managing the payment process, which itself becomes a component in a large-scale network implementing a complete online emporium. Scaling becomes a matter of arranging components on a graph rather than increased coupling within the codebase.

    But of course, there must be clear boundaries set for such a system. The most promising way to define components and graphs draws on functional programming and formal methods. For example, Malli-driven schemas provide a means of establishing checkpoints that the LLM agent cannot bypass. The agent must fulfill the contract by adhering to all the constraints and requirements.

    Once the high-level design is in place, these contracts can be issued to the agents in charge of the nodes in the graph. You no longer need an exceptionally capable agent that's able to comprehend a massive context and keep track of the interconnections across a sprawling application. The job description of the agent in charge of the flow of control is likewise dramatically circumscribed. The internal details of the cells can be ignored, with attention paid only to the graph itself. If the graph grows too complex, it, too, can be divided into separate, independent subgraphs. In this architecture, the context never needs to become unmanageably large.

    The Agent Synergy

    Historically, this kind of design has been hard to sell. Workflow engines and state-graph systems are not without their proponents, but they haven’t exactly swept the world. The basic problem is that they require a lot of additional ceremony. While wiring functions together by hand permits the programmer to forge ahead in a straight line, forcing yourself to step back, design a state graph, worry about the transition logic among disparate files, and code the glue just feels too onerous. Most programmers would much rather just bang out an if statement and keep going.

    But the picture changes completely when we introduce coding agents. A large language model lacks ego and has no difficulty writing boilerplate code. It does not find itself bored by ceremony, nor frustrated by the need for upfront structural planning. In fact, language models thrive on it. What is a tedious tax for a human developer serves as an explicit, unambiguous map for an agent. By embracing this structure, the agent secures the very boundaries it needs to stay coherent.

    Divorcing data flow from data transformation resolves the problem of LLM context overload in an elegant and general way. A strategic agent, in the form of the Conductor, coordinates the flow in the orchestration layer, plotting the tracks. Meanwhile, individual handler agents are responsible for writing, refining, and documenting their particular domain which is just a station on the railway. They do so within a safe, bounded context, avoiding the thicket of intractable problems posed by runaway cognitive saturation.

    Imagine a future in which coding agents assemble software systems by integrating and adapting existing workflows that have been approved by human oversight. We no longer have to suffer the frustration of constructing complex machines built out of opaque and unreliable components. We can rely on standardized and tested building blocks, following a clear and verifiable assembly plan, so that the resulting system is guaranteed by its very design to be correct, adaptable, and observable.

    Permalink

    State of Clojure surveys

    I checked the Previous Years links in the latest State of Clojure survey post and wrote down respondent counts by year.

    Here are the numbers:

    • 2010: 487
    • 2011: 670
    • 2012: 1372
    • 2013: 1061
    • 2014: 1339
    • 2015: 2445
    • 2016: 2420
    • 2017: absent…
    • 2018: 2325
    • 2019: 2461
    • 2020: 2519
    • 2021: 2527
    • 2022: 2382
    • 2023: 1761
    • 2024: 1549
    • 2025: 1545

    Or, as a graph:

    State of Clojure survey respondents by year

    What this suggests

    There is a very obvious shape:

    1. Rapid growth in early years
    2. Long plateau
    3. Drop in the last 3 years

    My take: these numbers are not a measure of absolute community size, but, within some error bars, they represent the trends of the community growth (or, recently, shrinkage). Looks like the trend is reversing to growth once again, though! That makes me hopeful 😊

    Permalink

    Copyright © 2009, Planet Clojure. No rights reserved.
    Planet Clojure is maintained by Baishamapayan Ghose.
    Clojure and the Clojure logo are Copyright © 2008-2009, Rich Hickey.
    Theme by Brajeshwar.