Just what IS Python, anyway?

A mental model for understanding Python’s role

Anyone who started programming in the early 1980s might have started with Apple II BASIC, BBC BASIC or Sinclair BASIC (ZX-81 or ZX Spectrum) - and 6502 or Z80 assembler.

Those early environments were all defined by immediacy. You typed something in; the machine did something. There was no ambiguity about what the language was for.

Since then a professional programmer might have ventured through FORTRAN, C, C++, UNIX shell, Visual Basic, VBA, VB.NET, C#, F#, JavaScript and more recently Rust, Zig, Nim and Odin. Every one of those fits elegantly into a mental slot: Systems language, Application language, Functional language, Runtime language or Tooling language.

Python, oddly, doesn’t. It can be tricky, for an experienced programmer, to grasp what it was and how it related to the other languages or which slot to put it in. Often, they will conclude "I don't like Python" and express confusion at its vast popularity - and even primacy - in the 2020s.

A slippery language

Traditionally we’re taught to classify languages along a few axes:

  • compiled vs interpreted
  • scripting vs “real” languages
  • imperative vs OO vs functional

Python fits poorly into all of them.

It isn't a compiled language in the C or Rust sense: it doesn't result in a standalone executable. But it isn't purely interpreted either, since it must be processed before execution. It supports imperative, object-oriented and functional styles but isn’t optimized for any of them. It began as a scripting language, but today it’s used to build large, long-running systems.

So just what IS Python?

Python is not a binary-producing language

The turning point is to realize that Python is not defined by the artefact it produces.

C, C++, Rust, Zig and Fortran produce binaries that can be directly run. The output is the thing. Once compiled, the language more or less disappears.

Python doesn’t work like that.

Python source code is compiled to bytecode, and that bytecode is executed by a virtual machine. The VM, the object model, the garbage collector and the standard library are not incidental. They are Python. A Python program needs this ecosystem to run; it can't run standalone, unless they are all bundled in with it.

In structural terms, Python sits alongside languages with runtimes and ecosystems:

  • .NET (C#, F#, VB.NET)
  • the JVM (Java, Scala, Kotlin, Clojure)

In all three cases, the runtime is the unit of execution, not the compiled artefact.

“Interpreted vs compiled” is therefore a false dichotomy. CPython parses Python source-code to an Abstract Syntax Tree (AST), compiles it to bytecode and then executes that bytecode on a VM. That’s not conceptually different from Java or .NET — just simpler and often slower, while the runtime startup overhead persists.

Python’s real role: orchestration

The solution to the puzzle of "What IS Python?" is to realize that Python is a runtime-centric language, whereupon its real role becomes obvious.

Python is not primarily about doing work. It’s about controlling work. It's exceptional good and for quickly lashing stuff together: like Lego, Meccano or snap-on tooling than traditional software construction.

The most important Python libraries — NumPy, SciPy, Pandas, PyTorch, TensorFlow — are not written in Python in any meaningful sense. Python provides the API, the glue and the control flow. The heavy lifting happens in underlying libraries written in C, C++, Fortran or CUDA - anything that can expose a C ABI (Application Binary Interface).

Python performs the same role over its libraries as:

  • SQL over databases
  • shell over Unix
  • VBA over Office

It is an orchestration language sitting above high-performance systems. That’s why it thrives in scientific computing, data pipelines and machine learning. It lets you build rapidly and easily, with simply syntax, whilst the underlying libraries deliver the performance. So long as orchestration overhead is low, Pythobn-based systems can scale surprisingly far.

Why Python still feels slippery

Even with this framing, Python can still feel oddly unsatisfying if you come from strongly structured languages.

Compared with .NET or the JVM, Python has:

  • weak static guarantees
  • loose module boundaries
  • a simpler, leakier object model

If you’re used to the discipline of C#, F# or Rust, Python can feel vague. Things work — until they don’t — and the language often declines to help you reason about correctness ahead of time.

It turns out that being able to throw things together quickly, in easy-to-understand code, and ecosystem breadth are far more important for mass adoption than type-safety, compilation, raw-performance or architectural rigidity. Make something easy, and more people will do it, more often.

Python's winning formula is to lower the barrier-to-entry for proof-of-concept and prototype stage projects - much like Visual BASIC and VBA did in the 1990s - and can even get to MVP (Minimum Viable Product). You can always make it faster, later, by translating critical paths into a compiled language.

Getting something working, at all and quickly, turns out to be hugely more important than getting it working fast or elegantly - something shell scripting showed us as far back as the 1970s.

Clearing up potential misunderstandings

Common misconceptions are worth addressing:

  • “Python is slow”
    Python orchestrates underlying code. In most applications, performance-critical paths live in native libraries. Only in a small number of domains — such as ultra-low-latency systems — does Python itself become the limiting factor.

  • “Python is a scripting language”
    Historically true, as that's how it originated, but it has evolved vastly since then.

  • “Python is interpreted”
    Better to say it is pre-compiled to bytecode that is then executed by a virtual machine.

A better language classification

A proper taxonomy therefore looks like this:

  1. Standalone native languages
    C, C++, Rust, Zig, Fortran
    → the binary is the product

  2. Runtime ecosystems
    Python, JVM languages, .NET languages
    → the runtime is the product

  3. Host-bound scripting languages
    Bash, PowerShell, VBA
    → the host environment is the product

Python belongs firmly in the second group.

A brief note for Rust and Go proponents

A common challenge from Rust or Go developers is that Python’s role is better served by “doing it properly” in a compiled language from the start.

That view makes sense — if your problem is well-specified, stable, performance-critical, and worth committing to upfront architectural constraints. In those cases, Rust or Go are often excellent choices - although these languages are more specialized than, say, C#, F# or JavaScript.

But many real-world problems do not start that way. They begin as ill-defined, exploratory or evolving systems: data pipelines, research code, internal tools, integration glue. A research-team needs to test an idea quickly in a small-scale way, rather than performantly on terabytes of data. A business-development team needs to solve a problem quickly and tactically, because the business needs a solution "yesterday". Some problems move to fast to wait for a strategic solution, or the cost of a strategic solution cannot yet be justified as too much is unknown. In those contexts, early commitment to strict typing, memory models or concurrency primitives can slow learning rather than accelerate it.

Python’s advantage is not that it replaces C#, Java, Rust or Go. It is that it defers commitment. You can explore the problem space quickly, validate assumptions, and only later decide which parts deserve the cost of rewriting in a compiled language. Your Proof-of-Concept or Prototype written in Python becomes your teacher and learning exercise.

In practice, Python and Rust and Go are not competitors but complements: Python for orchestration and discovery; Rust or Go for stabilised, performance-critical components where very specific issues need to be solved. Rust eliminates entire classes of bug to do with memory-management and threading; Go is superb at server-side services. These are not everyday programming needs.

Summary

Python isn’t confused, incoherent or a "toy" language. It simply departs from the mental models of earlier generations of languages and fulfills a unique role that no other language can quite match.

Python is not any of compiled, interpreted or “just a scripting language”. It's a runtime-centric orchestration layer and a complete ecosystem of its own. It's the Visual Basic and VBA of the internet era: ideal for rapid assembly, experimentation and leverage rather than purity or control.

And that makes it incredibly useful - and wildly popular.

Permalink

JAVA REALITY !!

▶ What Java Is ?

  • A verbose, boilerplate-heavy, object-oriented language.
  • Created during the dot-com bubble by Sun Microsystems.
  • Originally meant for TV remotes, released publicly in 1996.
  • Runs on billions of devices for decades.

▶ Java Philosophy

  • Motto: "Write once, run everywhere"
  • Reality: "Write once, debug everywhere"
  • Known for long logs, heavy configuration, and legacy systems.

▶ Ecosystem & Influence

  • Inspired JVM languages: Groovy, Clojure, Scala, Kotlin.
  • Also indirectly inspired JavaScript.
  • Failed attempt at the web: Java Applets (thankfully dead).

▶ Enterprise Reality

  • Often paired with Oracle Database.
  • Companies spend years talking about migrating away.
  • Expensive licenses, long-term enterprise lock-in.

▶ Getting Started Pain

  • Install JDK, JRE, JVM.
  • Prepare for massive error logs.
  • Memorize: public static void main(String[] args)

▶ Java Coding Style

  • Forced Object-Oriented Programming.
  • Requires classes even for Hello World.
  • Much more boilerplate than languages like Python.
  • Encourages deep inheritance and complex class hierarchies.

▶ Typical Java Project Lifecycle

  1. Write one giant class.
  2. Boss complains.
  3. Split into deeply nested subclasses.
  4. Code becomes impossible to refactor.
  5. Developer questions life choices.

▶ Final Truth

  • Java is widely hated yet widely used.
  • Two types of languages: 1) Those people complain about 2) Those nobody uses
  • Love it or hate it, Java gets real work done.

Permalink

About Dynamic Adding to Classpath in Clojure

Well, I guess people will just inevitably get into the problem of classpath, one way or another. The Classpath is a Lie described the problem very well: classpath is a lie. classpath, per se, is a simple list separated by colons, however, the real work is done by the Classloader.

Nevertheless, this isn't a post talking about classpath and ClassLoader. There are already a lot of great articles talking about it (links at the end of this post), and I can't claim I understand ClassLoaders to the extent that I can confidently teach others about it either.

This is a blog about what I have found during the process of trying to add new directories to classpath and require Clojure files in them at runtime. ClassLoader in Clojure is something very messy. The best strategy probably is to avoid the problem altogether. But still, if you really want to do it, I wish the following content can offer some help.

Use Builtin clojure.core/add-classpath

Clojure has a builtin add-classpath function. Although it has been deprecated, it works for simple use cases.

(defn check-dynamic-load []
  (let [tmp-dir (.toFile (Files/createTempDirectory "classpath-demo" (into-array java.nio.file.attribute.FileAttribute [])))
        tmp-clj (File/createTempFile "demo" ".clj" tmp-dir)
        tmp-name (subs (.getName tmp-clj)
                       0
                       (.lastIndexOf (.getName tmp-clj)
                                     "."))]
    ;; Add `tmp-dir` to `classpath` using builtin `add-classpath`.
    (add-classpath (.toURL tmp-dir))

    ;; Put a Clojure file under the directory
    (spit tmp-clj
          (str "(ns " tmp-name ") (def a 1)"))
                                        ;
    ;; `require` the Clojure file, and resolve the variable
    (assert (= 1 (var-get (requiring-resolve (symbol tmp-name
                                                     "a")))))

    ;; Update the Clojure file
    (spit tmp-clj
          (str "(ns " tmp-name ") (def a 2)"))

    ;; Reload the Clojure file
    (require (symbol tmp-name)
             :reload-all)

    ;; We can read the new value
    (assert (= 2 (var-get (requiring-resolve (symbol tmp-name
                                                     "a")))))
    (println "success")))

;; success
(check-dynamic-load)

If we evaluate the above code in a REPL or in cider, it works and prints "success". As we can see from the code, we can require a Clojure file whose path determined at runtime, and reload it to get the updated value.

However, there is a reason of it being deprecated. We can check its source code:

// The method used by clojure.core/add-classpath
static public void addURL(Object url) throws MalformedURLException{
      URL u = (url instanceof String) ? toUrl((String) url) : (URL) url;
      ClassLoader ccl = Thread.currentThread().getContextClassLoader();
      if(ccl instanceof DynamicClassLoader)
              ((DynamicClassLoader)ccl).addURL(u);
      else
              throw new IllegalAccessError("Context classloader is not a DynamicClassLoader");
}

It checks if the current thread's ContextClassLoader is a DynamicClassLoader. If so, it will call the DynamicClassLoader's addURL method. That means, this method will fail if there's some code set the current ContextClassLoader to something other than a DynamicClassLoader.

We expect add-classpath continues to work in case because the convention of setting a new ClassLoader is to set the current ClassLoader as the parent of the newly created ClassLoader. However, clojure.core/add-classpath only checks the current ClassLoader, more on this in the section.

(let [future
      (future
        (let [cl (.getContextClassLoader (Thread/currentThread))]
          (.setContextClassLoader (Thread/currentThread)
                                  (java.net.URLClassLoader. (into-array java.net.URL [])
                                                            cl)))
            (try (check-dynamic-load)
             (assert "unreachable")
             (catch Throwable t
               (println "dynamic loading failed"))))]
  ;; "dynamic loading failed"
  @future)

Use add-classpath from pomegranate

pomegranate provides a add-classpath that solves the problem described in the previous section.

The only thing we need to change is to replace clojure.core/add-classpath with cemerick.pomegranate/add-classpath.

(ns demo
  (:require
   [cemerick.pomegranate :as pomegranate]))

(defn check-dynamic-load-using-pomegranate []
  (let [;; ...
        ]
    ;; same code as `check-dynamic-load`
    (pomegranate/add-classpath (.toURL tmp-dir))
    ;; same code as `check-dynamic-load`
    ))

(let [future
      (future
        (let [cl (.getContextClassLoader (Thread/currentThread))]
          (.setContextClassLoader (Thread/currentThread)
                                  (java.net.URLClassLoader. (into-array java.net.URL [])
                                                            cl)))
        (check-dynamic-load-using-pomegranate)
        (print "success"))]
  ;; success
  @future)

Unlike add-classpath from clojure.core, pomegranate's add-classpath try to find the ClassLoader closest to the Primordial ClassLoader that is compatible with add-classpath, and call the addURL method from it.

;; in `add-classpath` function in pomegranate.clj  
(let [classloaders (classloader-hierarchy)]
      (if-let [cl (last (filter modifiable-classloader? classloaders))]
        (add-classpath jar-or-dir cl)
        (throw (IllegalStateException. (str "Could not find a suitable classloader to modify from "
                                            (mapv (fn [^ClassLoader c]
                                                    (-> c .getClass .getSimpleName))
                                                  classloaders))))))

Create a DynamicClassLoader When There isn't One

If you are running Clojure in REPL or with nrepl, the ContextClassLoader of the current thread will certainly be DynamicClassLoader, set by one of those tools. However, when you run clj command in a non-interactive manner, or use a AOT-compiled jar file, this wouldn't be the case.

This problem is quite easy to solve, we just need to set the ContextClassLoader to a DynamicClassLoader created by ourselves in the entrypoint of the program.

(defn -main [& args]
  (let [cl (.getContextClassLoader (Thread/currentThread))]
    (.setContextClassLoader (Thread/currentThread) (clojure.lang.DynamicClassLoader. cl)))
  ;; other code...
  )

When Using kaocha

So far, so good. Except when you finish the code and try to test some code and run it under the kaocha test runner. kaocha also did its own thing with ClassLoader and provides a add-classpath method. It breaks the previous method.

By detecting kaocha's presence and calling its add-classpath, alongside with the pomegranate one solves the issue for me.

(when (find-ns 'kaocha.classpath)
  ((intern 'kaocha.classpath
           'add-classpath)
   new-path))

Pitfall When Using a Threadpool

When you create a thread, the new thread will inherit the ContextClassLoader of the thread created it. However when you explicitly or implicitly (like when using future) use a threadpool, the executor may choose an existing thread, which could have a ContextClassLoader different from the calling thread.

You may consider creating a thread directly instead of relying on future in this case.

(doto (Thread/new
       (bound-fn []
         ;; code
         ))
  (.start))

A Few More Words

This post is definitely not comprehensive, and there are still a lot things I currently do not understand. The method I have described works for me for now. If you want to understand more about this topic, I have listed a few links below.

Permalink

Clojars Maintenance and Support: November/December 2025 Update

Critical Infrastructure: Clojars Maintentance and Support Update by Toby Crawley

November-December, 2025. Published January 24, 2026

This is an update on the work I’ve done maintaining Clojars in November and December of 2025.
Most of my work on Clojars is reactive, based on issues reported through the community or noticed through monitoring.

If you have any issues or questions about Clojars, you can find me in the #clojars channel on the Clojurians Slack, or you can file an issue on the main Clojars GitHub repository.

You can see the CHANGELOG for notable changes, and see all commits in the clojars-web and infrastructure repositories for this period. I also track my work over the years for Clojurists Together (and, before that, the Software Freedom Conservancy.

Below are some highlights for work done in November and December:

Permalink

jbundle

Package JVM applications into self-contained binaries. No JVM installation required to run the output.

jbundle transforms JVM applications (Clojure, Java, Kotlin, Scala, Groovy) into self-contained binaries. Previously known as clj-pack, the tool was renamed to reflect support for all JVM languages.

Motivation

The conventional deployment approach requires both the JAR and a JVM on the target machine. GraalVM native-image is an alternative, but presents challenges: slow compilations, complex reflection configuration, and library incompatibilities.

Permalink

ChronDB: Transforming a Clojure Database into a Polyglot Library with GraalVM Native Image and FFI

ChronDB was born as a server. A time-traveling key/value database with Git as its storage engine, exposing PostgreSQL wire protocol, Redis protocol, and REST/HTTP. You'd download the server, run it, connect with your favorite client. Classic architecture.

Then I started building spuff — ephemeral dev environments in the cloud. Spin up when needed, auto-destroy when forgotten. Written in Rust. For state management, I reached for SQLite. Simple, embedded, no server to manage. Just a file.

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

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?

P.S. You can try the live example at tonsky.me/stats. The data was imported from Nginx access logs, which I turned on and off on a few occasions, so it’s a bit spotty. Still, it should give you a general idea.

Permalink

Not Slop

Year in Review

A list of things that didn’t qualify as slop in 2025. There’s a loose thread of conviviality running through some of the things here that I would like to explore further in 2026.

Not Tech

Books

The Last Samurai

I can’t say that I’ve ever read anything quite like it. Both stunningly beautiful, laugh out loud funny, and full of fresh formal hijinks.

Benito Cereno

Melville saw clearly into the dark soul of America in a way that still sparks wonder and dread. A real historical event shaped into a flawless gem about freedom and its inverse.

The Once and Future King

I’ve been meaning to read this since I was 10 years old. The first book was an absolute delight. The second book was a complete slog. The third redeemed the second one by considerable measure. I took a break after that.

Parable of the Sower

Took way too long for me to get around to reading this. Reminded me of Cormac McCarthy’s The Road but way better and, you know, written thirteen years earlier. A very dark book and one to read closely in these interesting times.

Playing in the Dark: Whiteness and the Literary Imagination

Required reading for any lover of American literature. I don’t think about American writing in the same way on the other side of it.

Movies

Unknown Pleasures

One for the hard core film buffs. Two young men with absolutely no prospects and their misadventures. High art this one.

Music

Cocteau Twins

I did listen to a bit of music this year but not nearly as much as I should have. One standout is that I went through a good chunk of the Cocteau Twins catalog. I’m sad I arrived at it so late but also happy it’s fresh to my ears.

Autechre (Live)

I’m not a big Autechre head but thoroughly enjoyed seeing them live. They played in complete darkness for an hour and a half.

Writings Tools

Sailor Pro Gear Fountain Pen with 21K F nib

I’ve tried a bunch of fountain pens (Preppy, Kakuno, Lamy, TWISBI, Faber Castell) but the Sailor Pro Gear (and Slim) line is the only one I had any real connection with.

Midori B6 Notebook

After a year long flirt with the Hobonichi Cousin I decided it wasn’t for me. The paper and extra space is glorious but I take notes too sporadically and the planner format just wasn’t a good fit. The Midori B6 Notebook suits me better and the paper is glorious in its own way.

Music Tech

Convivial Hardware

Dirtywave M8 Model 2

A brilliant portable tracker labor of love created by one person. It recently got the ability to mount as a USB drive and stream 24 independent tracks of 24bit USB audio. It’s incredible what a 600mhz processor can do when there’s no cruft between you and your music.

Hordijk Modular System (2020)

Year five of living with this beast of a modern modular synth built using bespoke methods inspired by the original RA Moog Co. Factory. Nowhere near the bottom of this one. Rob Hordijk’s convivial approach to life and instrument building is something to strive for. He built his instruments so they could be learned and played over a lifetime.

Nerd Tech

Hardware

Kinesis Advantage360 Signature (Wireless)

Best computer related hardware upgrade I’ve made in 15 years. The 360 can be programmed via ZMK, I use Nick Coustos’ handy editor. One nice feature of a symmetrical keyboard is one-handed typing via mirroring. I’ve customized it so that holding down space presents the mirror layer.

The industrial design of the keyboard is a delight. I have two of them, one black with blank key caps and one black with steel blue key caps. I only need to charge the keyboard once every two months. Clearly built to last 10+ years, every bit of technology should be made this well.

DXT 3 Mouse

I spent some time with the Logictech Vertical MX mouse before switching. While the MX felt good, I don’t think it was actually was doing anything positive for my hands and the Logictech configuration app was effectively buggy malware. The DXT 3 design encourages holding it more like a stylus with a very light grip. I have two so I can easily switch hands. I’ve charged them once 6 months ago.

Software

SteerMouse

A lovely Japanese app for configuring your mouse. Just works.

Aerospace

A year and a half ago I got fed up with Apple’s window/app “management” tools and switched to Amethyst. Conceptually I liked it but it was just too buggy and slow. I switched to Aerospace: configuration in a text file and much snappier.

Emacs+

I used IntelliJ + Cursive for Clojure dev for a decade. Powerful combo and I would still highly recommend it for Clojure folks looking for a comprehensive IDE experience. But I never gave up on Emacs, all my thinking happens inside of org-mode. IntelliJ’s support for Emacs key bindings is impressive, but I finally got fed up with the various situations where it breaks down. I wiped my init.el clean and started rebuilding with modern packages. The new packages are simpler and significantly more composable. For Clojure work I have a fine set up without LSP, just Citre and flycheck-clj-kondo. I also picked up some new essential everyday tools: eww, avy, elpher, gnus, olivetti.

Permalink

Plenum in Kuala Lumpur

More late news: Rob Godman has taken Plenum to Kuala Lumpur for Plenum, Faraday Waves, Droplets and Pulse for the Evolving Matter: Artistic Process as Experimental Inquiry exhibition, Faculty of Creative Multimedia, Multimedia University. 10 November – 28 November 2025.

Permalink

Machine Learning with Clojure: Benefits & Perspectives

Why is Clojure starting to catch people’s attention for machine learning, even in a world where Python still dominates?

It is 2026 the buzz around machine learning has died down a bit. People are no longer chasing the next big thing; they just want tools that work. Python is everywhere, sure, but Clojure machine learning has found its place, especially among developers who value immutability, robust concurrency, and the speed of running on the JVM.

This guide explains what makes Clojure stand out for machine learning. It provides an overview of Clojure in this field, reviews commonly used ML libraries, and outlines how Clojure compares to Python.

Benefits of Clojure for Machine Learning and Data Science Projects

❶ Immutable Data Structures  

Clojure works with immutable, persistent data structures. When a dataset is created, it does not change unless a new one is made. This is a key factor in machine learning, where we can train and test models multiple times. 

Immutability guarantees that data cannot be changed unexpectedly, eliminating a common source of experimental error. In contrast, Python lists and arrays are mutable, which can lead to accidental state changes. Clojure’s immutable data structures ensure consistency and reliability throughout machine learning workflows.

❷ Superior Concurrency Via core. async  

Working with big datasets requires real parallel processing. Clojure’s core.async library steps in here. It uses channels to pass data between tasks, so we can eliminate stress about race conditions, locks, or weird bugs in shared state. This allows full focus on building the pipeline. 

This becomes even more useful while training multiple models simultaneously or streaming live data. Clojure ML Libraries takes full advantage of multicore CPUs, while Python often gets stuck with the GIL (Global Interpreter Lock), slowing things down. 

❸ REPL-Driven Development

Clojure’s REPL (Read-Eval-Print Loop) provides a live playground where it allows to write code, run it, and see the results immediately. Forget about waiting for long compiles or restarting the app. It helps to try ideas instantly.

This kind of feedback is a game-changer in machine learning and data science. Models always require some fine-tuning, and with the REPL, simply tweak a parameter, swap out an algorithm, or load a new dataset on the fly. The whole process stays quick and smooth.

It is also perfect for exploring. It helps to prototype, test out theories, and nail down solutions without wasting time.

Working this way brings people together, too. Teams swap code snippets, try things out side by side, and build trust in the models as they develop. While dealing with messy, complicated data pipelines, being able to experiment safely and efficiently matters a lot.

❹ Concise and Expressive

Clojure keeps things simple. The syntax is clean and avoids the extra code, keeping focus on the real logic. Less code is written, and it is easier to keep track of everything.

That simplicity really shines in data science and machine learning. It helps to transform data, filter values, or build full pipelines in just a handful of lines. Instead of getting stuck on repetitive chores, teams actually solve problems.

Clojure’s data tools are strong, too. Lists, maps, sequences- they are all built in and easy to use. It helps to work with data in a way that feels natural, and the code stays short and clear. For organizations, projects run faster, and bugs are fewer. The codebase also stays easy to manage.

❺ JVM Performance Benchmarks vs Python scikit-learn  

Performance is another area where Clojure really shines. Since it runs on the JVM, it benefits from Just-In-Time compilation and smart memory management. In real benchmarks, Clojure machine learning pipelines can actually beat Python’s scikit-learn, especially while running at scale across a bunch of machines. 

Plus, the JVM integrates well with most enterprise systems, making deployment smoother. If a team already uses Java or Scala, switching to Clojure just feels natural- and helps to skip the dependency headaches that come with Python.

Top Clojure ML Libraries in 2026

LibraryKey FeaturesUse Case
scicloj.mlPipelines, transformersSupervised learning
tech.ml.datasetData processing, GPU supportBig data preprocessing 
Deep DiamondNeural nets, CUDA integrationDeep learning dragan

scicloj.ml

This library gives modular pipelines and transformers. It’s built for supervised learning tasks. It connects with Smile and Tribuo to access a wide range of algorithms right out of the box- no extra setup.

tech.ml.dataset

Think of this as a dataframe library, but with GPU support. It is designed to handle large datasets and preprocess them before diving into training. If there is a lot of data, this tool helps keep things under control.

Deep Diamond

This is a neural network library that works with CUDA. It helps to train deep learning models on GPUs directly from Clojure. While building complex neural nets, this one just makes sense. 

Clojure Machine Learning vs Python: Key Comparisons

Concurrency Wins  

Clojure just handles concurrency better than Python. It is immutable data and core.async channels take many common issues out of running parallel tasks. Want to train big models or launch a bunch of experiments at once? Clojure handles it, and there is no need to worry about hidden side effects. 

Python, on the other hand, encounters problems with the Global Interpreter Lock (GIL). True parallel execution? Not really. People try to work around it with multiprocessing, but that just adds more complexity. With Clojure, concurrency feels clear and reliable.

Ecosystem Gaps  

Python definitely dominates when it comes to machine learning libraries: TensorFlow, PyTorch, scikit-learn, and the list keeps going. Clojure’s ecosystem is smaller, but there is momentum. Projects like SciCloj and Uncomplicate are developing new tools for data and visualization, and even GPU support. 

Sure, Python still gives more options, but Clojure’s libraries stick to functional programming ideas, so they fit together nicely. The gap is real, but it helps with steady progress from the Clojure community.

Production Deployment Ease  

Getting models into production is usually tougher than training them. Python projects often get messy due to dependency conflicts and version issues. 

Clojure runs on the JVM, which is everywhere in enterprise systems. That means deployment is smoother, especially if a team already uses Java or Scala. It helps integrate Clojure models into existing systems with minimal effort. For enterprise settings, this kind of compatibility is a real win over Python’s dependency-heavy stacks.

Few Real-World Clojure ML Use Cases and Success Stories

Nubank’s Model Deployment [https://bit.ly/49BXM2E]

Nubank is the top digital bank in Latin America, and it uses machine learning to detect fraud and perform financial modeling. Their team chose Clojure’s portability because it runs on the JVM, so it fit right into all their existing systems without much effort. Plus, Clojure’s functional programming principles keep the code clean and easy to maintain, which matters while dealing with sensitive stuff like financial workflows.

Thanks to this setup, they can deploy fraud-detection models quickly and with confidence. The code stays reliable, and the JVM keeps everything compatible. Clojure’s focus on immutability also means it can reproduce results- a big deal for banks that need everything traceable.

Flexiana’s Clojure Expertise 

Flexiana knows Clojure inside and out. We are a global team, and we focus on functional programming because it works- it actually solves real problems. When teams need to design and launch systems that can handle scale or build robust machine learning pipelines, they come to us. We don’t just talk theory; we help bridge that awkward gap between research and real-world production. For teams adopting Clojure ML libraries, Flexiana provides hands‑on engineering support.

We are also big on open source: we contribute, we share what we learn, and we work to make the whole Clojure community stronger. That helps companies bring Clojure into their business without all the usual headaches.

What These Stories Show 

  • OTTO proves Clojure can handle the heavy lifting for e-commerce, keeping complex ML pipelines both flexible and strong.  
  • Nubank demonstrates how Clojure operates in high-stakes industries like finance, where reliable and transparent systems are essential from end to end.
  • Flexiana, on the other hand, is not just another Clojure consulting company. We are deep in the community, building real-world systems and sharing what we know about functional programming.

Future Perspectives of Machine Learning with Clojure

2026 GPU/AI Advancements Via Uncomplicate  

In the future, machine learning withClojure is starting to feel pretty exciting, especially with the Uncomplicate ecosystem. Libraries like Neanderthal and Deep Diamond are pushing things forward.

  • Neanderthal takes care of fast linear algebra 
  • Deep Diamond brings neural network support with CUDA integration 

Put these together, and it helps to get a setup to train and run models directly on the GPU from Clojure. As GPUs get faster in 2026, these libraries are set to keep pace, making Clojure a strong choice for deep learning and large‑scale numerical work.

SciCloj Ecosystem Growth  

The SciCloj community is steadily expanding its ecosystem for data science and machine learning. The ecosystem is no longer just about pipelines and data preprocessing. Now, there are new libraries for 

  • Bayesian computing
  • Time series analysis
  • Probabilistic programming 

This means people are not just sticking to the basics- they are building specialized tools for complex statistical problems and real-world data. The modular approach helps a lot, too, as it helps to combine and match what is needed without heavy overhead. As all these pieces come together, Clojure is turning into a much more practical option for advanced machine learning research and even production work.

So, Why Does All This Matter? 

  • Uncomplicate shows that Clojure can remain competitive in the race for GPU-driven deep learning.
  • SciCloj shows the community can fill the gaps in serious statistical and probabilistic modeling. 

It is a good time to keep an eye on Clojure as machine learning continues to evolve.

Getting Started: Clojure ML Tutorial & Code Examples

For those just diving into machine learning with Clojure, check out scicloj.ml library. It is a great starting point. It helps avoid a ton of boilerplate, which is common in other languages. Instead, it helps to build pipelines in a clean, functional style. The whole focus is on the immutability and composition, so the code stays simple and easy to test.

Example: A Simple Supervised Learning Pipeline

What Is Happening Here?

  • Dataset loading is straightforward thanks to the built-in helpers.
  • It helps to split up features and targets right up front, so it is always clear what the model is actually learning.
  • Splitting data into train and test sets? The library handles it. No more manual shuffling.
  • Model training uses a simple, declarative call.
  • Evaluation returns metrics that can be inspected right away.

This little snippet shows just how much simpler things can be while using Clojure’s ML libraries. It helps to work with immutable data and small, predictable functions. No messy mutable states. No tangled class hierarchies. Just compose what is needed. That makes it easier to experiment and keeps production code more reliable.

Clojure for Machine Learning: FAQs for Beginners

Is Clojure good for ML?  

Absolutely. If developers are already familiar with functional programming, Clojure feels right at home for ML. Its focus on immutability and easy concurrency makes it great for building scalable machine learning projects.

Which Clojure ML libraries should Try First?  

Start with scicloj.ml for building ML pipelines. For working with data, tech.ml.dataset is great. If developers want GPU-powered deep learning, check out Deep Diamond.

How does Clojure compare to Python for ML?  

Python’s ML ecosystem is much bigger-  libraries like TensorFlow and PyTorch lead the way. Clojure’s ecosystem is smaller but growing. The benefits of Clojure for ML are cleaner concurrency, its functional design, and its strong integration with the JVM.

Is it possible to use Clojure ML libraries in the company’s systems?  

Yes, it is. Since Clojure runs on the JVM, it works smoothly with Java or Scala setups. That makes deployment in enterprise environments a lot simpler.

Is Clojure a good choice while just starting with machine learning?  

If developers are already comfortable with Clojure and just new to ML, it will be fine. The ML libraries here are built to be composable and functional, so it helps to experiment and learn by building small, clear steps.

What kind of ML tasks can I do with Clojure?  

Pretty much all the basics- classification, regression, clustering- plus deep learning. Scicloj.ml handles the standard stuff, and Deep Diamond handles the heavy neural networks.

Does Clojure support GPU acceleration?  

Yes. The Uncomplicate library family (like Neanderthal and Deep Diamond) brings GPU power to numerical computing and deep learning. 

Where to find and learn more about Clojure ML resources?  

Check out the SciCloj community. It helps with tutorials, study groups, and open-source projects- it is a good place to meet others and get real examples.

To Summing Up

By 2026, Clojure had really stepped up as a great alternative to Python for large-scale machine learning projects in the enterprise. It’s got some real benefits of Clojure for ML– immutability, easy concurrency, and solid JVM performance- that make it a natural fit when workflows need to be reproducible and able to scale.

Clojure’s machine learning libraries have not just appeared overnight; they have been growing steadily. It helps to get the full package, from simple models to advanced neural nets.

And this is not just theory. Just look at the real-world use cases. All of them show that Clojure is not just for experiment or side projects- it is solid enough for big, serious production work.

Looking forward, with its rock-solid language features, growing libraries, and active communities like SciCloj and Uncomplicate, Clojure’s future looks bright. It is not just following in Python’s footsteps; it is carving out its own path, focusing on clarity, modularity, and long-term stability. If a team cares about reproducible science and building reliable enterprise systems, it is time to give Clojure a real look.

Ready to move beyond experiments? Flexiana supports real‑world Clojure ML projects at scale.

The post Machine Learning with Clojure: Benefits & Perspectives appeared first on Flexiana.

Permalink

Converting map keys to nested namespace keys

Code

;; namespaced_keys.clj

(defn keyword-name [k]
  (clojure.string/replace (str (str k)) ":" ""))

(defn name-space
  ([nsp m]
   (if (map? m)
     (reduce
      (fn [acc [k v]]
        (assoc acc (keyword (keyword-name nsp) (keyword-name k)) v)) {} m)
     {nsp m}))

  ([m]
   (if (map? m)
     (let [map-keys (keys m)
           map-vals (vals m)]
       (apply merge (map name-space map-keys map-vals)))
     m)))

;; https://dnaeon.github.io/clojure-map-ks-paths/
(defn keys-in
  "Returns a sequence of all key paths in a given map using DFS walk."
  [m]
  (letfn [(children [node]
            (let [v (get-in m node)]
              (if (map? v)
                (map (fn [x] (conj node x)) (keys v))
                [])))
          (branch? [node] (-> (children node) seq boolean))]
    (->> (keys m)
         (map vector)
         (mapcat #(tree-seq branch? children %)))))

(defn map-depth [m]
  (apply max (map count (keys-in m))))

(defn deep-name-space
  ([m depth]
   (if (< depth 1)
     m
     (recur (name-space m) (dec depth))))

  ([m]
   (deep-name-space m (dec (map-depth m)))))

;;;;;;;;;;;;;;;;;;;;;;;;
;; Examples
;;;;;;;;;;;;;;;;;;;;;;;;

(keyword-name :user/a)
;;=> "user/a"

(name :user/a)
;;=> "a"

(defn some-fn [nsp acc k v]
  (assoc acc (keyword (keyword-name nsp) (keyword-name k)) v))

(some-fn :user {} :a 1)
;;=> #:user{:a 1}

(keys (some-fn :user {} :a 1))
;;=> (:user/a)

(defn some-fn-with-defaults [acc key-val]
  (let [k (first (keys key-val))
        v (first (vals key-val))]
    (some-fn :user acc k v)))

(some-fn-with-defaults {} {:a 1})
;;=> #:user{:a 1}

(reduce some-fn-with-defaults {} [{:a 1} {:b 2}])
;;=> #:user{:a 1, :b 2}

(name-space :user "john")
;;=> {:user "john"}

(name-space :user {:a 1})
;;=> #:user{:a 1}

(name-space :user {:a 1 :b 2})
;;=> #:user{:a 1, :b 2}

(name-space :user {:a 1 :b {:c 2}})
;;=> #:user{:a 1, :b {:c 2}}

(name-space
 (name-space :user {:a 1 :b {:c 2}}))
;;=> {:user/a 1, :user/b/c 2}

(deep-name-space {:user {:a 1 :b {:c 2}}} 1)
;;=> #:user{:a 1, :b {:c 2}}

(deep-name-space {:user {:a 1 :b {:c 2}}} 2)
;;=> {:user/a 1, :user/b/c 2}

(keys-in {:user {:a 1 :b {:c 2}}})
;;=> ([:user] [:user :a] [:user :b] [:user :b :c])

(map-depth {:user {:a 1 :b {:c 2}}})
;;=> 3

(deep-name-space {:user {:a 1 :b {:c 2}}})
;;=> {:user/a 1, :user/b/c 2}


(def deep-ns-map 
(deep-name-space {:user {:a 1 :b {:c 2}}}))

deep-ns-map
;;=> {:user/a 1, :user/b/c 2}

(last (keys deep-ns-map))
;;=> :user/b/c

(get deep-ns-map (last (keys deep-ns-map)))
;;=> 2

(:user/b/c deep-ns-map)
;;=> nil

(type :user/b/c)
;;=> clojure.lang.Keyword

(get deep-ns-map :user/b/c)
;;=> nil

(get deep-ns-map (keyword "user/b/c"))
;;=> nil

Permalink

Datomic at Clojure/Conj 2025

Happy belated new year! The Datomic team was among the six hundred participants at Clojure/Conj 2025. Here are some highlights.

Lessons from Production

A Decade on Datomic was full of lessons from Davis Shepherd and Jonathan Indig’s experience at Netflix. This is a technical talk with plenty of detail on how to leverage the semantics of a "deconstructed" database to design solutions for distributed systems challenges. We’ve put together a transcript for those who prefer a written version.

Tim Pote took us onto the warehouse floor in Forklifts, Facts, and Functions (transcript). He illustrated how Datomic’s flexible schema and queryable history gave him the leverage to solve bugs simply and easily.

Darlei Soares and João Nascimento showed how they used Datomic to model Nubank microservices in Immutable Knowledge Databases (transcript), revealing dependency patterns, risks and opportunities.

Amazing Day of Datomic

Our hands-on workshop is back. Based on Stuart Halloway’s original Day of Datomic, the event was led by Nubank engineers Hanna Figueiredo and Carol Silva.

Our priority for the session was to incorporate the latest features and architectural insights. Feedback was overwhelmingly positive, with 100% of participants reporting improved mastery, specifically praising the combination of architectural “deep dives” and practical challenges. We appreciate everyone who took part and made it so interactive. The workshop remains one of our favorite ways to help developers gain a solid base of understanding Datomic’s unique power.

Fresh Datomic Tools

Dustin Getz’s tour of A Datomic entity browser for prod (transcript) was electrifying. The tool is part programmable spreadsheet and part visual graph explorer, with features like query monitoring and a powerful query DSL to make a smooth developer experience. We’re eager to see where this power will be applied.

Datomic in Computational biology

Benjamin Kamphaus’s Power Tools for Translational Data Science (transcript) was a more philosophical talk. It weaves historical linguistics, computational phylogenetics, constraint satisfaction, and the biology of rare cancers into a parable on why it matters how we build our tools.

It builds on his prior talks about his data science toolkit, Clojure Where it Counts: Tidying Data Science Workflows (with Pier Federico) and Building a Unified Cancer Immunotherapy Data Library (with Lacey Kitch), both of which dive more into the technical advantages that Clojure and Datomic provide.

Some Datomic team members at Conj

Thanks to the organizers, speakers, and everyone who made the Conj such a vibrant time. We hope to see you there next year!

Permalink

Building web services with Duct

This article describes how to build a web service based on Clojure and the Duct framework. It covers all the necessary details of every part of Duct needed for this task. On completion, the reader should be able to write a web service from scratch with tests, configurations, and components calling 3rd party services.

The article is for intermediate programmers with a basic knowledge of web services and Clojure.

Clojure is a really different programming language compared to conventional languages like Java, Kotlin, Javascript, or functional languages like F#. The very first thing that everybody spots are its parenthesized prefix notation. The notation may look odd, but it has a lot of advantages compared to C-like syntax:

  • Compact syntax 
  • Simple syntax parser and highlighter
  • No priority of operators struggle
  • No breaking changes for new versions, due to new keyword/core function
  • Easy to read any code, everything is a function

The second special thing about Clojure’s environment is that there is no standard framework like Django, Ruby On Rails, Spring in other languages. Clojure lets a programmer compose a framework from small libraries. I guess that this decision is based on the idea that there is no one hammer for all problems. This comes with a lot of consequences.

Pros

  • A perfectly tailored framework to fit the problem.
  • No limitation to replace marshalling, HTTP, DB, routing and other libraries.
  • No overweight framework, only the parts used are in a project.
  • Great for microservices.

Cons

  • Hard at the beginning, experience with libraries needed.
  • Very hard for beginners without architecture skills.
  • Boilerplate code.
  • No scaffolding (like in Ruby On Rails)

This post is about Duct. The framework is light and composed from other (well-known in the Clojure world) small libraries (as is almost everything in Clojure). These are the main parts that are covered by Duct:

  • Configuration – local, production env., env. variables, …
  • HTTP handler with an application server
  • Database layer – a connection poll
  • Prepared middlewares for common security, HTTP headers, Content negotiation, …
  • Logging
  • Error handling
  • REPL – a code hot-swap

Architecture

The code

Let’s create a new project where we can see how to do general stuff with Duct. We are going to create a service for sending SMS messages.

We start by creating a project structure from Leiningen template by calling:

lein new duct sms +api +examplern[lukas@hel:~/dev/flexiana]$ lein new duct sms +api +examplernGenerating a new Duct project named sms...rnRun 'lein duct setup' in the project directory to create local config files.rn[lukas@hel:~/dev/flexiana]$

Let’s describe the command and what was created:

  • lein new generates a new Clojure project with Leiningen template
  • duct is a name of a template
  • sms is a name of a new project
  • +api is an option that adds middleware for APIs
  • +example is an option that adds some  example code

You can find more options in Duct’s README file https://github.com/duct-framework/duct#quick-start

Leiningen created a folder called sms. As we can see in the result above, the command

lein duct setup

will create configuration files for a local development and these files should not be watched by a version control system. The command prints out what files have been created:

[lukas@hel:~/dev/flexiana/sms]$ lein duct setuprnCreated profiles.cljrnCreated .dir-locals.elrnCreated dev/resources/local.ednrnCreated dev/src/local.cljrn[lukas@hel:~/dev/flexiana/sms]$

The template also generates the .gitignore file so you don’t have to alter the file manually.

The project structure

Leiningen generated a project from a template, let’s describe a project structure.

  • README.md: This is the obvious one, this file describes a project, contains installation and other useful notes.
  • dev: This folder contains files only for development mode. These files will not be part of a production JAR. It contains a configuration for the development and local environment (dev/resources/local.edn)
  • profiles.clj: Allows to override profiles.
  • project.clj: This is an important one. It contains project dependencies and plugins, build profiles, etc.
  • resources: contains static files like: project configuration, images, Javascripts, CSS, SQL, etc. These files will be a part of the production JAR.
  • src: contains all files that would be compiled: clj, cljc, cljs, cljx or java files
  • test: All the tests. These files will not be part of the production JAR.

We should be able to run the project as it is right now, because we passed +example option when we were generating the project. Let’s check it if it’s working. We can start the REPL as usual (lein repl), load a development profile (call (dev) in the repl) and start the server (we can call (go) or (reset)). Both of these functions start the server, in the following steps we will use reset, because it refreshes the code and restarts the server.

If everything went well, the output should be almost the same. The interesting information in the output is that the server started on port 3000 (we can change this in resources/sms/config.edn). The leiningen’s template created the example handler, so we can hit this URL http://localhost:3000/example.

As we can see, it works.

The routes

By default the project template generates routes to <project-name>.handler/example. This is just a convention, technically you can put the routes anywhere you want. Our example route is in sms.handler.example namespace, when you open a file you should see something like:

The code is pretty small, but there are a few new things. Let’s describe them.

First, there is an Integrant component defined by defmethod ig/init-key. Integrant is a micro-framework that allows you to create components and their configuration, and compose them together (you can think about it as a small DI framework). A component has a life-cycle, but for now init-key would be enough for us. As we can see from its name, init-key is called when the component is being initialized. The name of the component is a namespaced keyword :sms.handler/example and it should follow the code namespace. Integrant tries to load both variants of namespaces: sms.handler.example and sms.handler you can find more about it in the documentation. The last thing for the component is its configuration/options (this is a Clojure map, it could contain other components), but this is not important for now.

Second thing is the route itself. The route is defined by the Compojure library. The usage of the library is pretty simple and probably the simplest for beginners. The route is defined by the macros context and GET. Both macros are imported from compojure.core namespace (A side note :using :all is probably not a good idea, it’s hard to say if a function is from the same namespace, imported by :refer or :all, see more).

The Context macro allows you to wrap more routes with the same prefix to remove a path redundancy. The GET macro simply takes a path segment to match (in our example just /example), a parameters vector (we take none currently), and a response body or function. The response must be a valid Ring response (the simplest example is a map with :body and :status keys).

Now we know how the routes are defined, but how does the framework know that there are any routes? Let’s open the project’s configuration resources/sms/config.edn.

As we said above :sms.handler/example is the route component. As you can see the component takes a Clojure map. We can pass another dependency to the component by referencing it (e.g. #ig/ref :duct.database/sql). #ig/ref is  syntax sugar for referencing other components. In case you are curious about the details see the EDN documentation. If you want to see more details about it you can check the repository, but in short, it uses Hikari Connection Pool. We will not use a database in this article, so let’s move on.

On line 4 we can see a configuration for :duct.router/cascading, this component is a default router from the template and it takes a vector of references to other components. These components are route components. So the router component handles a connection between a request (Ring object) and the router itself.

The API

In the previous chapter, we described routes and their configuration. Let’s do some real work and add a resource for creating messages. Our resource handler will accept a message with the following keys:

  • receiver: a phone number as a String
  • text: a message text as a String

Let’s start with renaming the namespace sms.handler.example to sms.handler.api. We also need to rename the :sms.handler/example component in config.edn and remove the test namespace sms.handler.example-test. You can directly remove the namespace file because we will cover it later.

The handler file should be like:

(ns sms.handler.api
  (:require [compojure.core :refer compojure]
            [integrant.core :as ig]))

(defmethod ig/init-key :sms.handler/api [_ options]
  (compojure/context "/messages" []
    (compojure/GET "/" []
      {:body {:example "data"}})))


The configuration file should be like:

{:duct.profile/base
 {:duct.core/project-ns sms
  :duct.router/cascading
  [#ig/ref [:sms.handler/api]]
  :sms.handler/api
  {:db #ig/ref :duct.database/sql}}
 :duct.profile/dev #duct/include "dev"
 :duct.profile/local #duct/include "local"
 :duct.profile/prod {}
 :duct.module/logging {}
 :duct.module.web/api
 {}
 :duct.module/sql
 {}}

We can test the changes by calling curl http://localhost:3000/messages. If you have already started the REPL, you need to refresh the code in the JVM by calling (reset) from the REPL. Otherwise, it should work when you start the server.

The resource handler would accept a message with keys (receiver, text) and return the same message with the new key id (as String). Sending a message is not an idempotent process so we also need to change the HTTP method to post.

Let’s use TDD and start with defining the test. Create a file sms.domain.message.impl_test.clj with these contents:

(ns sms.domain.message.impl-test  
  (:require [clojure.test :as t]
            [sms.domain.message.impl :refer
             [map->MessageServiceImpl]]
            [sms.domain.message.sender :refer [Sender]]
            [sms.domain.message.service :as service])
  (:import [java.util UUID]))

(t/deftest send!-test
  (t/testing "should successfully send a message"
    (let [id (UUID/randomUUID)
          expected-message {:id id
                            :receiver "+420700000000"
                            :text "Hej Clojure!"}
          conf {:sender
                (reify Sender
                  (send! [_ message]
                    (t/is (= expected-message message))))}
          request (select-keys expected-message [:receiver :text])
          result (service/send! (map->MessageServiceImpl conf)
                                request)]
      (t/is (= expected-message result)))))

To be able to compile a test we need to also define the protocols and the implementation file. Let’s define sms.domain.message.sender namespace like:

(ns sms.domain.message.sender)

(defprotocol Sender
  (send! [this message]))

And the service itself sms.domain.message.service:

(ns sms.domain.message.service)

(defprotocol MessageService
  (send! [this message]))

And the implementation namespace sms.domain.message.impl:

(ns sms.domain.message.impl
  (:require [sms.domain.message.service :refer [MessageService]]))

(defrecord MessageServiceImpl []
  MessageService
  (send! [_ message]))

We did several things here. Let’s describe them to make them clear. First, we created a domain folder with a message subfolder. By that step, we made an explicit sign that the domain folder only contains our domain logic. A message is a domain object so everything related to it should be placed in sms.domain.message namespace. This structure increases the code cohesion because the functions that operate on the same data structures are together [BobCC]. When we would like to add another domain model, we just add another namespace or subfolder to the domain folder. Everybody who looks into the domain folder will see what the domain objects are, and where the functions that operate on them are.

We created a MessageSender protocol to separate behavior from implementation. The protocol provides several benefits:

  • The domain behavior is not tightly coupled with the sender’s implementation.
  • The protocol provides an explicit boundary between the domain logic and the rest of the system.
  • It’s easy to test domain objects and their behavior.
  • It’s possible to replace a sender at runtime. 

Now we should be able to run tests via REPL by calling (test) or from your editor or IDE. Don’t forget to (reset) the code after every change when you run the tests from the REPL. In one of our previous articles, we described how to work with REPL.

No matter how you call the tests, they should fail and the output should look like this:

Code overview with Duct n.1

And that’s good! Because we see that our code works and it fails in the expected state. As you can see the results says that a message map was expected but the function returned nil.

Now we can implement the resource handler of course we will not be implementing real message sending. Rather we would delegate sending messages to the Sender protocol, but before that we need to talk about boundaries.

Boundaries

When a system is being designed it’s a good practice to put the domain logic into the core of the system (ideally as pure functions) and move all communication with the outside world to the edges or boundaries of that system [BobCC]. This design approach has many advantages:

  • It makes explicit what is a part of the core and what is not.
  • It allows us to test the system components independently.
  • These boundaries can be replaced at runtime.
  • It allows us to develop the system even if we don’t know the boundary’s details.
  • This separation is done on the architecture and structure layer (project’s layout, files, …) of the system.

This may sound too abstract or theoretical, so let’s show it in an example. The Sender protocol defines a boundary. This boundary is shaped at the core of the system. But its implementation is shifted outside of the core of the system. E.g. an implementation could be placed in sms.boundaries.gateway namespace.

Another example of a boundary is the repository pattern [FowlerPEAA]. The repository provides a collection-like interface for accessing domain objects. Its implementation typically connects to a database.

The Sender with one method send! takes a configuration and message for sending. Now we can continue with implementing the system even if we don’t know the real API for sending messages.

Now we can define the expected Sender behavior in the test. The Only thing we know is that the API returns an ID of a sent message and we want to add this ID to the message and return it to the caller. In case of an error, it would return an error result. The updated test could look like this:

(ns sms.domain.message.impl-test
  (:require [clojure.test :as t]
            [sms.domain.message.impl :refer
             [map->MessageServiceImpl]]
            [sms.domain.message.sender :as sender]
            [sms.domain.message.service :as service])
  (:import [java.util UUID]))

(def ^:private id (UUID/randomUUID))

(def ^:private expected-message
  {:id id
   :receiver "+420700000000"
   :text "Hej Clojure!"})

(t/deftest send!-test
  (t/testing "should successfully send a message"
    (let [conf {:sender
                (reify sender/Sender
                  (send! [_ message]
                    (t/is (= (dissoc expected-message :id)
                             message))
                    (assoc message :id id)))}
          request (select-keys expected-message [:receiver :text])
          result (service/send! (map->MessageServiceImpl conf)
                                request)]
      (t/is (= expected-message result))))

  (t/testing "should return an error"
    (let [conf {:sender
                (reify sender/Sender
                  (send! [_ message]
                    {:error :unexpected-error}))}
          request (select-keys expected-message [:receiver :text])
          result (service/send! (map->MessageServiceImpl conf)
                                request)]
      (t/is (= {:error :unexpected-error} result)))))

We have defined the configuration for the API component with a boundary Sender. Actually we have reified the protocol (an anonymous implementation) in place just to simulate a response from the service and also a given message is asserted to the expected one (line 18).

If we run the tests again, nothing would change! Because we haven’t changed the handler’s implementation. So let’s update the implementation to use the Sender’s function send!.

(ns sms.domain.message.impl
  (:require [sms.domain.message.sender :as sender]
            [sms.domain.message.service :refer [MessageService]]))

(defrecord MessageServiceImpl [sender]
  MessageService
  (send! [_ request]
    (sender/send! sender request)))

There are a few new things. First, the function uses the Sender’s send! function. Second, a sender field was added to  MessageServiceImpl record.

If we run the tests now they should work.

Code overview with Duct n.2

Now we have finished the domain logic. The architecture may look too complicated for that simple task. But real systems are more complex and too complicated for the presentation.

Handling the HTTP requests

At this point, we have implemented the domain logic, but we don’t have any entry points to access the code. We are going to create an HTTP handler (or you can call it a controller). As in the previous section, we start with tests.

The test may look like this:

(ns sms.services.messages-test
  (:require [clojure.test :as t]
            [integrant.core :as ig]
            [ring.mock.request :as mock]
            [sms.domain.message.service :refer [MessageService]]
            [sms.handler.api])
  (:import [java.util UUID]))

(defn- send-message-api
  [conf params]
  (let [handler (ig/init-key :sms.handler/api conf)]
    (-> :post
        (mock/request "/messages")
        (assoc :body-params params)
        handler)))

(def ^:private expected-message
  {:id (UUID/randomUUID)
   :receiver "+420700000000"
   :text "Hej Clojure!"})

(t/deftest send-message-test
  (t/testing "should successfully send a message"
    (let [conf {:message-service
                (reify MessageService
                  (send! [_ message]
                    (t/is (= (select-keys expected-message
                                          [:receiver :text])
                             message))
                    expected-message))}
          params (select-keys expected-message [:receiver :text])
          {:keys [body status]} (send-message-api conf params)]
      (t/is (= expected-message body))
      (t/is (= 201 status))))

  (t/testing "should return 503, Sender failed"
    (let [conf {:message-service
                (reify MessageService
                  (send! [_ message]
                    {:error :unexpected-error}))}
          params (select-keys expected-message [:receiver :text])
          {:keys [status]} (send-message-api conf params)]
      (t/is (= 503 status)))))

Let’s also add the handler function, then we can compile the code:

(ns sms.handler.api.message)

(defn send! [message-service req])

When we run the tests, they should fail.

Code overview with Duct n.3

We should probably describe the test file and some of its interesting parts. We have defined the send-message-api function that wraps the API call. It calls ig/init-key on the API components with some configuration (the same thing happens when the application’s server starts), the application calls a post request with the given params.

The test itself is pretty simple, we have defined the component’s configuration as conf with one key called message-service. Under that key, we reified (mocked) the service protocol to expected behavior. We have made actual calls of the API and assert the result (body and status in this case).

The second test is almost the same as the first one, except the service returns an error result.

The very simple implementation of the handler may look like:

(ns sms.handler.api.message
  (:require [ring.util.response :as response]
            [sms.domain.message.service :as service]))

(defn send! [message-service req]
  (let [result (service/send! message-service
                              (select-keys req
                                           [:receiver :text]))]
    (if (= {:error :unexpected-error} result)
      (response/status {} 503)
      (response/created (format "/messages/%s" (:id result))
                        result))))

Now tests should be green.

Code overview with Duct n.4

Production implementation

Well, we have implemented the HTTP handler and also added tests. But if we call the handler e.g. via curl

curl localhost:3000/messages -X POST -H 'Content-Type: application/json' -d '{"receiver": "+4207000000000", "text": "Hej Clojure!"}'

We would get something like this:

java.lang.IllegalArgumentException: No implementation of method: :send! of protocol: #'sms.domain.message.service/MessageService found for class: nil

The Message Service needs production implementation. Don’t be afraid we are not going to implement a real SMS sender. Instead, we will forward the message to another service via HTTP. First, let’s add a new dependency to the project HTTP-Kit, the latest stable version is 2.3.0. Update project.clj and restart the REPL, yes this is really needed. Start the REPL and create a file src/sms/boundaries/sms_gateway.clj with the following content:

(ns sms.boundaries.sms-gateway
  (:require [duct.logger :as logger]
            [integrant.core :as ig]
            [jsonista.core :as jsonista]
            [org.httpkit.client :as http]
            [sms.domain.message.sender :refer [Sender]]))

(def mime-type "application/json")

(def default-headers
  {"Accept" mime-type
   "Content-Type" mime-type})

(defrecord SmsGateway [logger url]
  Sender
  (send! [_ message]
    (let [{:keys [body status] :as response}
          (http/post url
                     {:as :text
                      :body (jsonista/write-value-as-string message)
                      :headers default-headers})]
      (case status
        (200 201) (jsonista/read-value body)
        (do
          (logger/log logger :error response)
          {:error :unexpected-error})))))

(defmethod ig/init-key :sms.boundaries/sms-gateway
  [_ opts]
  (map->SmsGateway opts))

On line 14 the Sender protocol is implemented. The record takes a logger instance and a url of the remote service. An implementation of send! The method is pretty straightforward, the API is called and if a response is successful (HTTP code 200 or 201) the response is parsed, otherwise, the response is logged and :error is returned. On line 27 the Integrant component is defined. The component converts a given configuration to a new record Sms Gateway.

Now we have a component for the Sender, but we also need a component for MessageService to be able to pass the Sender’s implementation. Let’s create a file sms.services.message with the following content:

(ns sms.services.message
  (:require [integrant.core :as ig]
            [sms.domain.message.impl :refer
             [map->MessageServiceImpl]]))

(defmethod ig/init-key ::service
  [_ opts]
  (map->MessageServiceImpl opts))

The code is very simple. The Integrant component just creates an implementation of MessageService.

Let’s initialize the component in the project’s configuration. Let’s update resources/sms/config.edn like this:

{:duct.profile/base
 {:duct.core/project-ns sms
  :duct.router/cascading
  [#ig/ref [:sms.handler/api]]
  :sms.boundaries/sms-gateway
  {:logger #ig/ref :duct/logger
   :url #duct/env ["SMS_GATEWAY_URL" Str]}
  :sms.services.message/service
  {:sender #ig/ref :sms.boundaries/sms-gateway}
  :sms.handler/api
  {:message-service #ig/ref :sms.services.message/service}}
 :duct.profile/dev #duct/include "dev"
 :duct.profile/local #duct/include "local"
 :duct.profile/prod {}
 :duct.module/logging {}
 :duct.module.web/api
 {}
 :duct.module/sql
 {}}

On line 7 the SmsGateway component is initialized with the Duct’s logger and the SMS Gateway URL that is taken from an environment variable called SMS_GATEWAY_URL (as String). On line 11 the MessageService component is initialized with a sender.

This usage of Inversion of Control allows us to easily change the Sender’s implementation in tests, when the system starts, and even at runtime.

Unfortunately in local development we probably aren’t able to connect to the real SMS Gateway. But fortunately, we can fix this problem by creating a mock of the gateway. Let’s create a silly mock of the gateway dev/src/sms_dev/boundaries/sms_gateway_mock.clj

(ns sms-dev.boundaries.sms-gateway-mock
  (:require [integrant.core :as ig]
            [sms.domain.message.sender :refer [Sender]])
  (:import [java.util UUID]))

(defrecord SmsGatewayMock []
  Sender
  (send! [_ message]
    (assoc message :id (UUID/randomUUID))))

(defmethod ig/init-key :sms-dev.boundaries/sms-gateway-mock
  [_ opts]
  (map->SmsGatewayMock opts))

As you can see the silly implementation of Sender protocol just put an id on a given message to simulate the real behavior.

Now we have to tell the Duct to use SmsGatewayMock just for local development. Update dev/resources/local.edn file:

{:sms-dev.boundaries/sms-gateway-mock {}
 :sms.services.message/service
 {:sender #ig/ref :sms-dev.boundaries/sms-gateway-mock}}

On the very first line, a SmsGatewayMock is created and on another line, we tell the :sms.services.message/service to use the SmsGatewayMock as a Sender.

Now if we restart the REPL or call (reset) in the REPL, the code will be refreshed and we should be able to call the API e.g. via curl:

curl localhost:3000/messages -X POST -H 'Content-Type: application/json' -d '{"receiver": "+4207000000000", "text": "Hej Clojure!"}'

If everything went well the output will  look like this:

{"receiver":"+4207000000000","text":"Hej Clojure!","id":"097bd803-c3fb-4d4a-beeb-bc381ec4e4d8"}

As you can see we have sent the message and a new id has been put into that message. Because we added the SmsGatewayMock to the dev folder the mock will not be part of the production JAR (all files from the dev folder will not be included too).

Secret magic

You may be wondering how is it possible that a request’s body was parsed to a map. There is one important thing we haven’t described yet. The Duct has got modules with default configurations for common middlewares like Ring defaults. The module is named :duct.module.web/api and it’s initialized in the project’s configuration. If you’re curious you can check the internals and see that it uses Muuntaja for marshalling.

The module puts middleware around :duct.core/handler (the main handler called from the HTTP servlet) when the system starts. This is default behaviour and it can be overridden, but we don’t want to go so far in this article.

When the Muuntaja middleware sees Content-Type in a request’s headers it can try to negotiate and parse the request (in our case to a map). If a request doesn’t have any known Content-Type, the attribute :body-params on the request object will be nil. 

As you can see now technically there is no hidden magic, but a beginner could be surprised or confused.

Production

We have implemented the web service and now we want to run it in a staging or production environment. We need to have a stand-alone file that can be run with JDK. Let’s run the following command: lein uberjar

[lukas@hel:~/dev/flexiana/sms]$ lein uberjar
Compiling sms.boundaries.sms-gateway
Compiling sms.domain.message.impl
Compiling sms.domain.message.sender
Compiling sms.domain.message.service
Compiling sms.handler.api
Compiling sms.handler.api.message
Compiling sms.main
Compiling sms.services.message
Created /Users/lukas/dev/flexiana/sms/target/sms-0.1.0-SNAPSHOT.jar
Created /Users/lukas/dev/flexiana/sms/target/sms-0.1.0-SNAPSHOT-standalone.jar

Now we can take the sms-0.1.0-SNAPSHOT-standalone.jar and run it everywhere, where JDK is installed.

You can simply test it from a command line. Before running it the Gateway URL must be set via an environment variable. Let’s try it:

[lukas@hel:~/dev/flexiana/sms]$ export SMS_GATEWAY_URL="http://localhost:8080/smsgateway"
[lukas@hel:~/dev/flexiana/sms]$ java -jar target/sms-0.1.0-SNAPSHOT-standalone.jar 
20-05-25 10:28:01 hel REPORT [duct.server.http.jetty:13] - :duct.server.http.jetty/starting-server {:port 3000}

As you can we have set the SMS_GATEWAY_URL variable and run the application. In the command line, we can see that the application has started on port 3000. So if the gateway service was running on port 8080 we would be able to test it by posting a message to http://localhost:3000/messages.

Duct uses by default Timbre for logging. In the development profile, all logs go to logs/dev.log file. But in the production, all logs are sent to stdout.

Conclusion

This article has introduced a Clojure framework, Duct, that helps programmers with building server-side applications. The Duct parts have been briefly described with the necessary details. All the described Duct’s parts have been presented with practical examples and tests.

A reader should be able to create a new server-side application, implement HTTP handlers, and call 3rd party APIs. Also, the reader has been taught how to write tests. Optionally run the tests from the favorite editor and at the end how to build a deployable JAR file and configure it.

This article has not exhausted all of Duct’s possibilities. There are more things that could be explained in future articles like:

  • Communication with a database
  • Data validation
  • Schedulers
  • etc.

Bibliography

[BobCA]: Clean Architecture, Robert C. Martin, Prentice Hall, 2017

[BobCC]: Clean Code, Robert C. Martin, Prentice Hall, 2009

[FowlerPEAA]: Patterns of Enterprise Application Architecture, Martin Fowler, Addison-Wesley, 2003

[XUnit]: XUnit Test Patterns: Refactoring Test Code, Gerard Meszaros, Addison-Wesley, 2007

The post Building web services with Duct appeared first on Flexiana.

Permalink

Annually-Funded Developers' Update: November and December 2025

Hello Fellow Clojurists!

This is the sixth and final report from the 5 developers who received Annual Funding in 2025. You can review their reports from throughout the year here:

Jan/Feb 2025
March/April 2025
May/June 2025
July/Aug 2025
Sept/Oct 2025

Thanks everyone for the fantastic work!

Dragan Djuric: Neanderthal, Deep Diamond, Diamond ONNX Runtime
Eric Dallo: ECA, clojure-lsp
Michiel Borkent: clj-kondo, Reagami, Squint, babashka, SCI, and more…
Oleksandr Yakushev: CIDER nREPL, Orchard, clj-async-profiler, Virgil
Peter Taoussanis: Telemere, Tufte, Sente, Tempel, Carmine, Trove


Dragan Djuric

2025 Annual Funding Report 6. Published January 4, 2026.

My goal with this funding in 2025 is to support Apple silicon (M cpus) in Neanderthal (and other Uncomplicate libraries where that makes sense and where it’s possible).

Having achieved a lot of the goals for this project in 2025 in the previous 10 months, in this funding period, my main effort was concentrated on the icing on the cake.

  • I improved support for various data types in Deep Diamond (including :double)
  • improved the implementation of the info method in DD
  • released Deep Diamond 0.41.0
  • improved cuda handling of :long descriptors
  • extended Neanderthal Vector with the support for Tensor protocols,
  • extended Neanderthal Matrix with the support for Tensor protocols,
  • simplified the code in various places,
  • wrote new tests,
  • GE matrices are now also tensors
  • fixed the transfer support for integer matrices
  • implementated integer kernels for vectors and GE matrices in CUDA,
  • updated Apple BNNS tensor to be compatible with the changes in the last 3-4 months,
  • fixed leftover reflections in the old BNNS-based engine in DD
  • released Neanderthal 0.60.0,
  • released Deep Diamond 0.42.0,
  • Update upstream onnxruntime to 1.23.2, also build it locally with CUDA to update diamond-onnxrt,
  • released diamond-onnxrt 0.21.0

I released several new versions of Uncomplicate libraries with these user-facing improvements including:

  • Deep Diamond
  • Neanderthal
  • Diamond ONNX Runtime

Hammock time. Did some research on LLMs and the future Clojure implementation of high-level LLMs based on Diamond ONNX Runtime

I also wrote a tutorial on dragan.rocks. I had plans to write more, but couldn’t find time and energy, since December was especially full with lectures at the university, and I did not want to risk burnout :)


Eric Dallo

2025 Annual Funding Report 6. Published January 6, 2026.

What a year! This was probably the year that I most commited and worked in some many things for Clojure community, all of that thanks to ClojuristsTogether, thank you very much! In November I met so many people at ClojureConj that I work for 5 years and never met personally, it was a very good feeling to talk about so many subjects, projects, and hear about the best people in Clojure community. Check it out the picture with the biggest names in developer tooling for Clojure!

image

(From left to right: Eric Dallo, Rich Hickey, Peter Strömberg, Michiel Borkent, and Arthur Fücher)

I spent some time preparing my talk that I gave there about ECA which should be available soon for anyone interested.

eca

ECA is growing even faster, with more people using, testing, finding bugs, asking for features, and **I’m confident to say that after 6 months, ECA is a tool as good comparing with big players in the market, being free, OSS, written in Clojure and so much extensible, I’m really happy with the result so far and there are so many ideas and improvements I wanna do next year thanks to community sponsor and support!

0.78.2 - 0.87.2

  • Add workspaces to /doctor
  • Improve LLM request logs to include headers.
  • Add openai/gpt-5.1 to default models.
  • Fix regression exceptions on specific corner cases with log obfuscation.
  • Fix absolute paths being interpreted as commands. #199
  • Remove non used sync models code during initialize. #100
  • Fix system prompt to mention the user workspace roots.
  • Improve system prompt to add project env context.
  • Add support to rollback messages via chat/rollback and chat/clear messages. #42
  • Add new models to GitHub config (Gpt 5.1 and Opus 4.5).
  • Update anthropic default models to include opus-4.5
  • Update anthropic default models to use alias names.
  • Fix binary for macos amd64. #217
  • Support rollback file changes done by write_file, edit_file and move_file. #218
  • Improve rollback to keep consistent UI before the rollback, fixing tool names and user messages.
  • Support nested folder for rules and commands. #220
  • Fix custom tools output to return stderr when tool error. #219
  • Support dynamic string parse (${file:/path/to/something} and ${env:MY_ENV}) in all configs with string values. #200
  • Improve /compact UI in chat after running, cleaning chat and showing the new summary.
  • Better config values dynamic string parse:
    • Support ${classapath:path/to/eca/classpath/file} in dynamic string parse.
    • Support ${netrc:api.foo.com} in dynamic string parse to parse keys. #200
    • Support default env values in ${env:MY_ENV:default-value}.
    • Support for ECA_CONFIG and custom config file.
  • Deprecate configs:
    • systemPromptFile in favor of systemPrompt using ${file:...} or ${classpath:...}
    • urlEnv in favor of url using ${env:...}
    • keyEnv in favor of key using ${env:...}
    • keyRc in favor of key using ${netrc:...}
    • compactPromptFile in favor of compactPrompt using ${classpath:...}
  • Fix ${netrc:...} to consider :netrcFile config.
  • Fix ${netrc:...} to consider :netrcFile config properly.
  • Enhanced hooks documentation with new types (sessionStart, sessionEnd, chatStart, chatEnd), JSON input/output schemas, execution options (timeout)
  • Fix custom tools to support argument numbers.
  • Improve read_file summary to mention offset being read.
  • Enhanced hooks documentation with new types (sessionStart, sessionEnd, chatStart, chatEnd), JSON input/output schemas, execution options (timeout)
  • Support rollback only messages, tool call changes or both in chat/rollback.
  • Fix backwards compatibility for chat rollback.
  • Support providers <provider> httpClient version config, allowing to use http-1.1 for some providers like lmstudio. #229
  • Support openai/gpt-5.2 and github-copilot/gpt-5.2 by default.
  • Improve agent behavior prompt to mention usage of editor_diagnostics tool. #230
  • Use selmer syntax for prompt templates.
  • Support Google Gemini thought signatures.
  • Support gemini-3-pro-preview and gemini-3-flash-preview models in Google and Copilot providers.
  • Fix deepseek reasoning with openai-chat API #228
  • Support ~ in dynamic string parser.
  • Support removing nullable values from LLM request body if the value in extraPayload is null. #232
  • Improve read-file summary to show final range properly.
  • Improve model capabilities for providers which model name has slash: my-provider/anthropic/my-model
  • Fix openai-chat tool call + support for Mistral API #233
  • Skip missing/unreadable @file references when building context
  • Fix regression: /compact not working for some models. Related to #240

clojure-lsp

We finally support vertical alignment in clojure-lsp format via cljfmt, one of the most requested features!
I started a sequence of lots of outdated bumps which fixed some issues and intend to finish on the next release as some break tests and some existing features. Also, we had some new features like squint projects support!

2025.11.28-12.47.43

  • New keywords completion inside namespaced maps. #2113
  • Pass current namespace aliases to cljfmt when range-formatting. #2129
  • bump clj-kondo to 2025.10.24-20251120.193408-8 improving performance, fixing false-positives and supporting java inner classes.
  • Bump cljfmt to 0.15.5 - adding support for vertical alignment.
  • Support squint projects when having squint.edn. #2158
  • Support find definition of java inner classes (Foo$Bar). #2157
  • Bump babashka/fs to 0.5.28.
  • Bump opentelemetry to 1.51.0.

Michiel Borkent

2025 Annual Funding Report 6. Published January 6, 2026.

In this post I’ll give updates about open source I worked on during November and December 2025.

To see previous OSS updates, go here.

Sponsors

I’d like to thank all the sponsors and contributors that make this work possible. Without you, the below projects would not be as mature or wouldn’t exist or be maintained at all! So a sincere thank you to everyone who contributes to the sustainability of these projects.

gratitude

Current top tier sponsors:

Open the details section for more info about sponsoring.

Sponsor info

If you want to ensure that the projects I work on are sustainably maintained, you can sponsor this work in the following ways. Thank you!

Updates

Clojure Conj 2025

Last November I had the honor and pleasure to visit the Clojure Conj 2025. I met a host of wonderful and interesting long-time and new Clojurians, many that I’ve known online for a long time and now met for the first time. It was especially exciting to finally meet Rich Hickey and talk to him during a meeting about Clojure dialects and Clojure tooling. The talk that I gave there: “Making tools developers actually use” will come online soon.

presentation at Dutch Clojure meetup

(From left to right: Steven Lombardi, Eric Dallo, Rich Hickey, Peter Strömberg, Michiel Borkent, Burin Choomnuan, and Arthur Fücher).

Babashka conf and Dutch Clojure Days 2026

In 2026 I’m organizing Babashka Conf 2026. It will be an afternoon event (13:00-17:00) hosted in the Forum hall of the beautiful public library of Amsterdam. More information here. Get your ticket via Meetup.com (currently there’s a waiting list, but more places will come available once speakers are confirmed). CfP will open mid January. The day after babashka conf, Dutch Clojure Days 2026 will be happening. It’s not too late to get your talk proposal in. More info here.

Clojurists Together: long term funding

I’m happy to announce that I’m among the 5 developers that were granted Long term support for 2026. Thanks to all who voted! Read the announcement here.

Projects

Here are updates about the projects/libraries I’ve worked on in the last two months in detail.

  • babashka: native, fast starting Clojure interpreter for scripting.

    • Bump process to 0.6.25
    • Bump deps.clj
    • Fix #1901: add java.security.DigestOutputStream
    • Redefining namespace with ns should override metadata
    • Bump nextjournal.markdown to 0.7.222
    • Bump edamame to 1.5.37
    • Fix #1899: with-meta followed by dissoc on records no longer works
    • Bump fs to 0.5.30
    • Bump nextjournal.markdown to 0.7.213
    • Fix #1882: support for reifying java.time.temporal.TemporalField (@EvenMoreIrrelevance)
    • Bump Selmer to 1.12.65
    • SCI: sci.impl.Reflector was rewritten into Clojure
    • dissoc on record with non-record field should return map instead of record
    • Bump edamame to 1.5.35
    • Bump core.rrb-vector to 0.2.0
    • Migrate detecting of executable name for self-executing uberjar executable from ProcessHandle to to native image ProcessInfo to avoid sandbox errors
    • Bump cli to 0.8.67
    • Bump fs to 0.5.29
    • Bump nextjournal.markdown to 0.7.201
  • SCI: Configurable Clojure/Script interpreter suitable for scripting

    • Add support for :refer-global and :require-global
    • Add println-str
    • Fix #997: Var is mistaken for local when used under the same name in a let body
    • Fix #1001: JS interop with reserved js keyword fails (regression of #987)
    • sci.impl.Reflector was rewritten into Clojure
    • Fix babashka/babashka#1886: Return a map when dissociating a record basis field.
    • Fix #1011: reset ns metadata when evaluating ns form multiple times
    • Fix for https://github.com/babashka/babashka/issues/1899
    • Fix #1010: add js-in in CLJS
    • Add array-seq
  • clj-kondo: static analyzer and linter for Clojure code that sparks joy.

    • #2600: NEW linter: unused-excluded-var to warn on unused vars in :refer-clojure :exclude (@jramosg)
    • #2459: NEW linter: :destructured-or-always-evaluates to warn on s-expressions in :or defaults in map destructuring (@jramosg)
    • Add type checking support for sorted-map-by, sorted-set, and sorted-set-by functions (@jramosg)
    • Add new type array and type checking support for the next functions: to-array, alength, aget, aset and aclone (@jramosg)
    • Fix #2695: false positive :unquote-not-syntax-quoted in leiningen’s defproject
    • Leiningen’s defproject behavior can now be configured using leiningen.core.project/defproject
    • Fix #2699: fix false positive unresolved string var with extend-type on CLJS
    • Rename :refer-clojure-exclude-unresolved-var linter to unresolved-excluded-var for consistency
    • v2025.12.23
    • #2654: NEW linter: redundant-let-binding, defaults to :off (@tomdl89)
    • #2653: NEW linter: :unquote-not-syntax-quoted to warn on ~ and ~@ usage outside syntax-quote (`) (@jramosg)
    • #2613: NEW linter: :refer-clojure-exclude-unresolved-var to warn on non-existing vars in :refer-clojure :exclude (@jramosg)
    • #2668: Lint & syntax errors in let bindings and lint for trailing & (@tomdl89)
    • #2590: duplicate-key-in-assoc changed to duplicate-key-args, and now lints dissoc, assoc! and dissoc! too (@tomdl89)
    • #2651: resume linting after paren mismatches
    • clojure-lsp#2651: Fix inner class name for java-class-definitions.
    • clojure-lsp#2651: Include inner class java-class-definition analysis.
    • Bump babashka/fs
    • #2532: Disable :duplicate-require in require + :reload / :reload-all
    • #2432: Don’t warn for :redundant-fn-wrapper in case of inlined function
    • #2599: detect invalid arity for invoking collection as higher order function
    • #2661: Fix false positive :unexpected-recur when recur is used inside clojure.core.match/match (@jramosg)
    • #2617: Add types for repeatedly (@jramosg)
    • Add :ratio type support for numerator and denominator functions (@jramosg)
    • #2676: Report unresolved namespace for namespaced maps with unknown aliases (@jramosg)
    • #2683: data argument of ex-info may be nil since clojure 1.12
    • Bump built-in ClojureScript analysis info
    • Fix #2687: support new :refer-global and :require-global ns options in CLJS
    • Fix #2554: support inline configs in .cljc files
  • edamame: configurable EDN and Clojure parser with location metadata and more
    Edamame: configurable EDN and Clojure parser with location metadata and more

    • Minor: leave out :edamame/read-cond-splicing when not splicing
    • Allow :read-cond function to override :edamame/read-cond-splicing value
    • The result from :read-cond with a function should be spliced. This behavior differs from :read-cond + :preserve which always returns a reader conditional object which cannot be spliced.
    • Support function for :features option to just select the first feature that occurs
  • squint: CLJS syntax to JS compiler

    • Allow macro namespaces to load "node:fs", etc. to read config files for conditional compilation
    • Don’t emit IIFE for top-level let so you can write let over defn to capture values.
    • Fix js-yield and js-yield* in expression position
    • Implement some? as macro
    • Fix #758: volatile!, vswap!, vreset!
    • pr-str, prn etc now print EDN (with the idea that you can paste it back into your program)
    • new #js/Map reader that reads a JavaScript Map from a Clojure map (maps are printed like this with pr-str too)
    • Support passing keyword to mapv
    • #759: doseq can’t be used in expression context
    • Fix #753: optimize output of dotimes
    • alength as macro
  • reagami: A minimal zero-deps Reagent-like for Squint and CLJS

    • Performance enhancements
    • treat innerHTML as a property rather than an attribute
    • Drop support for camelCased properties / (css) attributes
    • Fix :default-value in input range
    • Support data param in :on-render
    • Support default values for uncontrolled components
    • Fix child count mismatch
    • Fix re-rendering/patching of subroots
    • Add :on-render hook for mounting/updating/unmounting third part JS components
  • NEW: parmezan: fixes unbalanced or unexpected parens or other delimiters in Clojure files

  • CLI: Turn Clojure functions into CLIs!

    • #126: - value accidentally parsed as option, e.g. --file -
    • #124: Specifying exec fn that starts with hyphen is treated as option
    • Drop Clojure 1.9 support. Minimum Clojure version is now 1.10.3.
  • clerk: Moldable Live Programming for Clojure

    • always analyze doc (but not deps) when no-cache is set (#786)
    • add option to disable inline formulas in markdown (#780)
  • scittle: Execute Clojure(Script) directly from browser script tags via SCI

  • Nextjournal Markdown

    • Add config option to avoid TeX formulas
    • API improvements for passing options
  • cherry: Experimental ClojureScript to ES6 module compiler

    • Fix cherry compile CLI command not receiving file arguments
    • Bump shadow-cljs to 3.3.4
    • Fix #163: Add assert to macros (@willcohen)
    • Fix #165: Fix ClojureScript protocol dispatch functions (@willcohen)
    • Fix #167: Protocol dispatch functions inside IIFEs; bump squint accordingly
    • Fix #169: fix extend-type on Object
    • Fix #171: Add satisfies? macro (@willcohen)
  • deps.clj: A faithful port of the clojure CLI bash script to Clojure

    • Released several versions catching up with the clojure CLI
  • quickdoc: Quick and minimal API doc generation for Clojure

    • Fix extra newline in codeblock
  • quickblog: light-weight static blog engine for Clojure and babashka

    • Add support for a blog contained within another website; see Serving an alternate content root in README. (@jmglov)
    • Upgrade babashka/http-server to 0.1.14
    • Fix :blog-image-alt option being ignored when using CLI (bb quickblog render)
  • nbb: Scripting in Clojure on Node.js using SCI

    • #395: fix vim-fireplace infinite loop on nREPL session close.
    • Add ILookup and Cons
    • Add abs
    • nREPL: support "completions" op
  • neil: A CLI to add common aliases and features to deps.edn-based projects.

    • neil.el - a hook that runs after finding a package (@agzam)
    • neil.el - adds a function for injecting a found package into current CIDER session (@agzam)
    • #245: neil.el - neil-executable-path now can be set to clj -M:neil
    • #251: Upgrade library deps-new to 0.10.3
    • #255: update maven search URL
  • fs - File system utility library for Clojure

    • #154 reflect in directory check and docs that move never follows symbolic links (@lread)
    • #181 delete-tree now deletes broken symbolic link root (@lread)
    • #193 create-dirs now recognizes sym-linked dirs on JDK 11 (@lread)
    • #184: new check in copy-tree for copying to self too rigid
    • #165: zip now excludes zip-file from zip-file (@lread)
    • #167: add root fn which exposes Path getRoot (@lread)
    • #166: copy-tree now fails fast on attempt to copy parent to child (@lread)
    • #152: an empty-string path "" is now (typically) understood to be the current working directory (as per underlying JDK file APIs) (@lread)
    • #155: fs/with-temp-dir clj-kondo linting refinements (@lread)
    • #162: unixify no longer expands into absolute path on Windows (potentially BREAKING)
    • Add return type hint to read-all-bytes
  • process: Clojure library for shelling out / spawning sub-processes

    • #181: support :discard or ProcessBuilder$Redirect as :out and :err options

Contributions to third party projects:

  • ClojureScript
    • CLJS-3466: support qualified method in return position
    • CLJS-3468: :refer-global should not make unrenamed object available

Other projects

These are (some of the) other projects I’m involved with but little to no activity happened in the past month.

Click for more details - [pod-babashka-go-sqlite3](https://github.com/babashka/pod-babashka-go-sqlite3): A babashka pod for interacting with sqlite3
- [unused-deps](https://github.com/borkdude/unused-deps): Find unused deps in a clojure project
- [pod-babashka-fswatcher](https://github.com/babashka/pod-babashka-fswatcher): babashka filewatcher pod
- [sci.nrepl](https://github.com/babashka/sci.nrepl): nREPL server for SCI projects that run in the browser
- [babashka.nrepl-client](https://github.com/babashka/nrepl-client)
- [http-server](https://github.com/babashka/http-server): serve static assets
- [nbb](https://github.com/babashka/nbb): Scripting in Clojure on Node.js using SCI
- [sci.configs](https://github.com/babashka/sci.configs): A collection of ready to be used SCI configs.
- [http-client](https://github.com/babashka/http-client): babashka's http-client
- [html](https://github.com/borkdude/html): Html generation library inspired by squint's html tag
- [instaparse-bb](https://github.com/babashka/instaparse-bb): Use instaparse from babashka
- [sql pods](https://github.com/babashka/babashka-sql-pods): babashka pods for SQL databases
- [rewrite-edn](https://github.com/borkdude/rewrite-edn): Utility lib on top of
- [rewrite-clj](https://github.com/clj-commons/rewrite-clj): Rewrite Clojure code and edn
- [tools-deps-native](https://github.com/babashka/tools-deps-native) and [tools.bbuild](https://github.com/babashka/tools.bbuild): use tools.deps directly from babashka
- [bbin](https://github.com/babashka/bbin): Install any Babashka script or project with one command
- [qualify-methods](https://github.com/borkdude/qualify-methods)
- Initial release of experimental tool to rewrite instance calls to use fully qualified methods (Clojure 1.12 only)
- [tools](https://github.com/borkdude/tools): a set of [bbin](https://github.com/babashka/bbin/) installable scripts
- [babashka.json](https://github.com/babashka/json): babashka JSON library/adapter
- [speculative](https://github.com/borkdude/speculative)
- [squint-macros](https://github.com/squint-cljs/squint-macros): a couple of macros that stand-in for [applied-science/js-interop](https://github.com/applied-science/js-interop) and [promesa](https://github.com/funcool/promesa) to make CLJS projects compatible with squint and/or cherry.
- [grasp](https://github.com/borkdude/grasp): Grep Clojure code using clojure.spec regexes
- [lein-clj-kondo](https://github.com/clj-kondo/lein-clj-kondo): a leiningen plugin for clj-kondo
- [http-kit](https://github.com/http-kit/http-kit): Simple, high-performance event-driven HTTP client+server for Clojure.
- [babashka.nrepl](https://github.com/babashka/babashka.nrepl): The nREPL server from babashka as a library, so it can be used from other SCI-based CLIs
- [jet](https://github.com/borkdude/jet): CLI to transform between JSON, EDN, YAML and Transit using Clojure
- [lein2deps](https://github.com/borkdude/lein2deps): leiningen to deps.edn converter
- [cljs-showcase](https://github.com/borkdude/cljs-showcase): Showcase CLJS libs using SCI
- [babashka.book](https://github.com/babashka/book): Babashka manual
- [pod-babashka-buddy](https://github.com/babashka/pod-babashka-buddy): A pod around buddy core (Cryptographic Api for Clojure).
- [gh-release-artifact](https://github.com/borkdude/gh-release-artifact): Upload artifacts to Github releases idempotently
- [carve](https://github.com/borkdude/carve) - Remove unused Clojure vars
- [4ever-clojure](https://github.com/oxalorg/4ever-clojure) - Pure CLJS version of 4clojure, meant to run forever!
- [pod-babashka-lanterna](https://github.com/babashka/pod-babashka-lanterna): Interact with clojure-lanterna from babashka
- [joyride](https://github.com/BetterThanTomorrow/joyride): VSCode CLJS scripting and REPL (via [SCI](https://github.com/babashka/sci))
- [clj2el](https://borkdude.github.io/clj2el/): transpile Clojure to elisp
- [deflet](https://github.com/borkdude/deflet): make let-expressions REPL-friendly!
- [deps.add-lib](https://github.com/borkdude/deps.add-lib): Clojure 1.12's add-lib feature for leiningen and/or other environments without a specific version of the clojure CLI


Oleksandr Yakushev

2025 Annual Funding Report 6. Published January 8, 2026.

Hello friends! Here’s my update on November-December 2025 Clojurists Together work. I’ve dedicated time in equal parts to improving clj-async-profiler and to working on nREPL/CIDER stack.

clj-async-profiler

I’ve released the first beta version of the quite disruptive 2.0.0 release of the profiler. The new version contains a whole new type of graph - a heatgraph - which incorporates time as a separate dimension, and allows constructing on-demand flamegraphs over arbitrary time slices. This new version also migrates from profiles as collapsed TXT files to the industry-standard JFR (Java Flight Recorder) profiles which incorporate more data and are more compact and efficient.

  • Released 2.0.0-beta with support for JFR profiler data and heatgraphs.
  • 45: Released version 1.7.0 for the older pre-2.0.0 branch of the profiler to restore compatibility with Linux kernerl 6.17.

nREPL

nREPL 1.6.0-alpha1 has been prepared which contains many updates that improve the stability of nREPL and simplify its codebase. nREPL is continually moving towards its original goal of being a simple reliable foundation for other tools to build upon.

  • #403: Fix off by 1 error in CallbackBufferedOutputStream.
  • #408: Refactor stdin middleware.
  • #409: Refactor handler construction and middleware application.

Orchard

Plenty of work has gone into Orchard, mostly on the inspector and doc/Javadoc side. These changes will go into the next named CIDER release.

  • #362: Info: don’t crash if symbol contains $ or /.
  • #363: Inspector: show duplicates in analytics.
  • #365: Inspector: don’t datafy vars.
  • #367: Inspector: pretty-print arrays distinctively from vectors.
  • #369: Inspector: group methods by declaring class.
  • #370: Inspector: dedicated view for methods.
  • #371: Inspector: analytics improvements.
  • #368: Print: correctly render empty records.

cider-nrepl

Here I worked on the dependency footprint of the library. Cider-nrepl has many dependencies and a complicated solution to managing and shading them.

  • #956: [ci] Include nrepl dependency into releases again.
  • #959: Stop shading Compliment and clj-reload.

Virgil

Released Virgil 0.5.1.

  • #45: Address reflection warnings.
  • #46: Enable linting on CI.

Peter Taoussanis

2025 Annual Funding Report 6. Published January 9, 2026.

Hi everyone 👋

Another year behind us! I hope everyone had a peaceful break, and managed to get some quality time with family/friends/pets ^^

2025 was a productive year for my Clojure projects with >36 non-trivial library releases and one talk (“Effective Open Source Maintenance Maintenance”).

Some highlights included:

My main unifying themes for the year were observability and documentation. I’m pretty happy now with the combo of Telemere, Trove, Tufte, and Truss. These work well together as a sort of opinionated observability suite for serious Clojure applications. And I think that together they offer a pretty compelling demonstration of some of the kinds of real-world advantages Clojure can offer people trying to get things done at scale.

As always, a very big and warm thank you to the many users and contributors that helped patiently test, give feedback, report bugs, and offer improvements over the year. The Clojure community has always been and continues to be a special one ❤️

Likewise a big thank you to the companies and folks (incl. Clojurists Together, Nubank, and other sponsors) that have helped support my OSS work financially! It’s been great being able to dedicate so much time to open source, and it’s something I feel very grateful to have been able to benefit from 🙏

Those closely following my releases may have noted that I was unusually quiet over Nov/Dec. tl;dr ended up with some unexpected Life stuff coming up that has required the bulk of my attention and energy. So a few releases I had planned for that period will need to wait until later (likely Q2 2026).

Relatedly, I should warn that I might need to deload a little in 2026. Will still be providing maintenance and support as usual, just not sure yet to what extent I’ll be able to contribute the usual amount of attention to substantial greenfield work.

I’m in Clojure for the long haul, and will continue to be present and as active as I’m able- things may just be a little bit more unpredictable on my side for the next few months as I see how next developments play out.

Best wishes to everyone for the new year, and much love to you all! 🫶

- Peter Taoussanis

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.