When You Get to Be Smart Writing a Macro

Day-to-day programming isn’t always exciting. Most of the code we write is pretty straightforward: open a file, apply a function, commit a transaction, send JSON. Finding a problem that can be solved not the hard way, but smart way, is quite rare. I’m really happy I found this one.

I’ve been using hashp for debugging for a long time. Think of it as a better println. Instead of writing

(println "x" x)

you write

#p x

It returns the original value, is shorter to write, and doesn’t add an extra level of parentheses. All good. It even prints original form, so you know which value came from where.

Under the hood, it’s basically:

(defn hashp [form]
  `(let [res# ~form]
     (println '~form res#)
     res#))

Nothing mind-blowing. It behaves like a macro but is substituted through a reader tag, so defn instead of defmacro.

Okay. Now for the fun stuff. What happens if I add it to a thread-first macro? Nothing good:

user=> (-> 1 inc inc #p (* 10) inc inc)
Syntax error macroexpanding clojure.core/let at (REPL:1:1).
(inc (inc 1)) - failed: vector? at: [:bindings] spec: :clojure.core.specs.alpha/bindings

Makes sense. Reader tags are expanded first, so it replaced inc with (let [...] ...) and then tried to do threading. Wouldn’t fly.

We can invent a macro that would work, though:

(defn p->-impl [first-arg form fn & args]
  (let [res (apply fn first-arg args)]
    (println "#p->" form "=>" res)
    res))

(defn p-> [form]
  (list* 'p->-impl (list 'quote form) form))

(set! *data-readers* (assoc *data-readers* 'p-> #'p->))

Then it will expand to

user=> '(-> 1 inc inc #p-> (* 10) inc inc)

(-> 1
  inc
  inc
  (p->-impl '(* 10) * 10)
  inc
  inc)

and, ultimately, work:

user=> (-> 1 inc inc #p-> (* 10) inc inc)
#p-> (* 10) => 30
32

Problem? It’s a different macro. We’ll need another one for ->>, too, so three in total. Can we make just one instead?

Turns out you can!

Trick is to use a probe. We produce an anonymous function with two arguments. Then we call it in place with one argument (::undef) and see where other argument goes.

Inside, we check where ::undef lands: first position means we’re inside ->>, otherwise, ->:

((fn [x y]
   (cond
     (= ::undef x) <thread-last>
     (= ::undef y) <thread-first>))
 ::undef)

Let’s see how it behaves:

(macroexpand-1
  '(-> "input"
     ((fn [x y]
        (cond
          (= ::undef x) <thread-last>
          (= ::undef y) <thread-first>))
      ::undef)))

((fn [x y]
   (cond
     (= ::undef x) <thread-last>
     (= ::undef y) <thread-first>))
   "input" ::undef)

(macroexpand-1
  '(->> "input"
     ((fn [x y]
        (cond
          (= ::undef x) <thread-last>
          (= ::undef y) <thread-first>))
      ::undef)))

((fn [x y]
   (cond
     (= ::undef x) <thread-last>
     (= ::undef y) <thread-first>))
   ::undef "input")

If we’re not inside any thread first/last macro, then no substitution will happen and our function will just be called with a single ::undef argument. We handle this by providing an additional arity:

((fn
   ([_]
    <normal>)
   ([x y]
    (cond
      (= ::undef x) <thread-last>
      (= ::undef y) <thread-first>)))
   ::undef)

And boom:

user=> #p (- 10)
#p (- 10)
-10

user=> (-> 1 inc inc #p (- 10) inc inc)
#p (- 10)
-7

user=> (->> 1 inc inc #p (- 10) inc inc)
#p (- 10)
7

#p was already very good. Now it’s unstoppable.

You can get it as part of Clojure+.

Permalink

A look into Nubank’s tech hub in Berlin

Today, Nu is one of the largest digital financial services platforms in the world, with over 118 million customers across Brazil, Mexico, and Colombia. From day one, our goal has been to challenge the status quo of the financial industry—using proprietary technology and data to build innovative, easy-to-use products that actually solve real problems.

Our mission is simple: to fight financial complexity and empower people. That’s why we create end-to-end solutions that support customers throughout their entire financial journey—promoting access, transparency, and real progress through responsible credit.

With an efficient and scalable business model, we’re able to combine low operational costs with sustainable growth. This approach has earned international recognition in rankings like Time’s “100 Most Influential Companies,” Fast Company’s “Most Innovative Companies,” and Forbes’ “World’s Best Banks.”

So why having a tech hub at Berlin?

Our mission is to give people back control of their financial lives. We make the extraordinary happen by consistently challenging the status quo and driving innovation.

Some pillars support us to achieve this goal – and technology is certainly one of them.

Many of our teams need to analyze large datasets to make the best decisions for the customer – be it, for example, develop a product, make our service smarter and more efficient or better understand how people interact with our services. And this is where our technology hub in Berlin, Germany, opened in late 2017, comes in.

Situated in the Mitte neighborhood, synthesis of the past, the present and the future of Berlin, the office is home to a growing number of cross-functional teams. They are dedicated to maintain and evolve a world-class self-service data platform, enabling nearly every team at Nubank to leverage data for automated decision making, financial control and reporting, strategic analysis, etc.

Our Berlin office includes meeting rooms equipped with Zoom and Google Meet features, phone booths for focused individual work, and a game room designed for relaxation and team-building interactions.

The data infrastructure teams

Our data infrastructure teams are responsible for building services that are scalable and resilient for processing, managing, and monitoring large-scale distributed data processing systems. That includes designing well thought and documented APIs, backed by scalable data ingestion, processing, and serving systems. 

We make heavy use of Nubank’s standard tech stack, including building Clojure microservices. But given our focus on data processing, we also continually explore and leverage big data technologies for batch and streaming processing, building machine learning models, etc.

Our data infrastructure teams do not build data pipelines, models, or analyses for specific business areas. Instead, we provide a managed self-service platform that empowers business analysts, data scientists, analytics engineers, and many other roles to leverage data with autonomy, supporting a high-performance culture aligned with Nubank’s values.

In addition to our data infrastructure teams, a growing number of teams from Nubank’s wider Engineering Horizontal organization have a presence in the Berlin tech hub. They are focused on areas such as core infrastructure automation, developer experience, databases, mobile development platform, distributed systems R&D, among others.

Why open a tech hub in Berlin?

There are several reasons for having a team in Germany.

The first one is that Berlin is a fantastic city not only for the quality of life, but also for being a center of activities related to technology. The city is a hub for events, companies and data processing research – and it is also a central point that gives easy access to other cities in Europe.

Another reason is that the city has a friendly infrastructure for foreigners and many inhabitants speak English – which greatly facilitates communication.

Finally, and related to the previous points, investing in a European tech hub allows us to foster one of our main values, that is to build strong and diverse teams. At Nubank, diversity and collaboration help us achieve extraordinary outcomes, as we’re committed to being a good reference for career growth.

Nubank’s work at our Berliner tech hub

The atmosphere in the office is very similar to that of our São Paulo headquarters. We don’t have a formal dress code (wearing shorts and flip flops is absolutely normal) and we encourage anyone who wants to bring their pets to work.

As Nubank is a company that actively seeks diversity at the workplace and gender equality, Berlin is a perfect blend of a great pool of talent with diverse backgrounds.

Nowadays, we have over 40 Nubankers from twelve nationalities (and growing), coming from three different continents. We arrived at the office from Brazil, France, Italy, India, Poland, United States, Peru, Chile, Portugal, Ukraine, Croatia and, clearly, Germany. 

Nubank operates as a global company. In addition to our offices in the markets where we currently operate—Brazil, Mexico, and Colombia—and our tech hub in Berlin, we also have technology centers in Uruguay and the United States, home to Cognitect (creators of Clojure and Datomic), which became part of Nubank in 2020.

In Berlin, our way of working is built around small, autonomous teams. That means more agility, independent decision-making, and less day-to-day bureaucracy. But autonomy doesn’t mean isolation—we remain deeply connected to the rest of Nubank, sharing the same values, the same mission, and a strong commitment to creating real impact in people’s lives.

We’re looking for diverse technical talent to help build this vision with us—from software engineers to data infrastructure specialists, from product leaders to systems engineers. Here, every person plays a key role in building financial solutions that are simpler, more accessible, and truly innovative for millions of customers.

The post A look into Nubank’s tech hub in Berlin appeared first on Building Nubank.

Permalink

May 2025 Short-Term Project Updates Q2 2025 Projects

This is the April/May project update for four of our Q2 2025 Funded Projects. (Reports for the other two are on a different schedule). A brief summary of each project is included to provide overall context.

Jeremiah Coyle: Bling
Add support for using hiccup to style and format messages, a template string syntax to style and format messages, and 1-3 additional formatting templates for callouts, headers, and points-of-interest.

Brandon Ringe: CALVA
A new REPL output view for Calva, which is a webview in VS Code. The webview will allow us to add more rich features to the output webview, while also providing better performance.

Bozhidar Batsov: CIDER
Provide continued support for CIDER, nREPL and the related libraries (e.g. Orchard, cidernrepl, etc) and improve them in various ways.

Jeaye Wilkerson: Jank
Build jank’s seamless C++ interop system.

Bling: Jeremiah Coyle

Q2 2025 $2k. Report No. 1, Published 1 June 2025


Upon commencing Q2 work on Bling, I reordered the list of priorities set forth in the initial project description.
Updated TO DO list:
All the primary goals listed above are realized in [`v0.8.0`](https://clojars.org/io.github.paintparty/bling/versions/0.8.0).

Some highlights…

Figlet banners:

Docs here

Specialized template for Malli validation errors

This is experimental, with more work to be done on optimizing for disjunctions and cases with multiple errors on same value.

Docs here

Bling offers bling.explain/explain-malli to present Malli validation errors:

(require '[bling.explain :refer [explain-malli]])

(def Address
  [:map
   [:id string?]
   [:tags [:set keyword?]]
   [:address
    [:map
     [:street string?]
     [:city string?]
     [:zip int?]
     [:lonlat [:tuple double? double?]]]]])

(explain-malli
 Address
 {:id "Lillan",
  :tags #{:coffee :artesan :garden},
  :address
  {:street "Ahlmanintie 29", :zip 33100, :lonlat [61.4858322 87.34]}})

The above code would print the following:

You can also pass an option trailing options map to customize the appearance of the printed output. In the example below, we are leaving out the display of the schema within the callout block:

(explain-malli
 Address
 {:id "Lillan",
  :tags #{"coffee" :artesan :garden},
  :address
  {:city "Tempare" :street "Ahlmanintie 29", :zip 33100, :lonlat [61.4858322 87.34]}}
 {:display-schema? false})

The above code would print the following:

High Fidelity Printing

Docs here

Bling offers bling.hifi for colorized pretty-printing of Clojure, Java, and JavaScript data structures. bling.hifi/hifi will return an ansi-sgr decorated string, while bling.hifi/print-hifi will print such a string.

Under the hood, the formatting/colorizing is achieved with Fireworks.

By default, the theme of this output will be Universal Neutral.

If you set a valid BLING_MOOD env var, the theme of the hifi output will be Alabaster Light or Alabaster Dark.

You can choose one of the other available themes by following the instructions in the Fireworks readme and setting up a config.edn on your system, with a corresponding BLING_CONFIG env var. This config will also let you control many other aspects of the formatting with the hifi output.


Alabaster Dark        
Alabaster Light        

Check out the other available themes here


Enhanced contrast

You can set a BLING_MOOD env var to enhance the contrast of bling-formatted printings.

Docs here.


Brandon Ringe: CALVA

Q2 2025 $9K. Report 1, Published 13 May 2025.

The new REPL output view was released! It’s an opt-in feature for now and is not currently the default REPL output destination. It’s currently in a very MVP state. I’ll be fixing bugs and adding features in the coming weeks.

This output view is web-based (a VS Code webview), and is implemented entirely in ClojureScript. We use replicant for the UI.

There’s project board on GitHub that I’ve been using to track my work on this. I wanted the issues to be draft issues until the first version was released. I’ll be converting all of them to issues soon.

Here are a few issues I intend to tackle in the coming weeks:

Documentation:

Thanks to Thomas Heller for helping us (some time ago) explore ways of integrating ClojureScript and TypeScript in Calva. Thanks to Christian Johansen for replicant.


Bozhidar Batsov: CIDER

Q2 2025 $9K. Report 1, Published 16 May 2025.

The last month extremely fruitful for CIDER and friends and resulted in many notable releases and improvements. Below are some of the highlights:

  • clojure-ts-mode 0.3
    • lots of improvements to the font-locking and indentation logic
    • indentation rules are now consistent with cljfmt
  • clojure-ts-mode 0.4
    • regular expressions are font-locked with a Tree-sitter grammar
    • adds support for aligning forms
    • implements many of the refactoring commands from clojure-mode
  • CIDER 1.18
  • refactor-nrepl 3.11.0
    • Before this update the project was broken on Clojure 1.12
    • clj-refactor.el was also updated to use the latest version of the middleware

The were also several releases of cider-nrepl and orchard, related to CIDER 1.18. Work on CIDER 1.19 is already underway, and I’m also happy to report that clojure-ts-mode is now in a pretty good shape for general use.


Jeaye Wilkerson: Jank

Q2 2025 $9K. Report 1, Published 7 May 2025.

Thank you!

To start with, thank you, Clojurists Together, for the sponsorship this quarter. This is the largest income I have this year, so far, so it really makes a difference.

Seamless C++ interop

This quarter, I’m focusing on seamless interop with C++ from jank. Doing this from a lisp is unprecedented. One month in, I have implemented the following.

  1. JIT compiling arbitrary C++ code
  2. Reaching into C++ values
  3. Constructing stack-allocated C++ values
  4. Overload resolution
  5. Implicit conversions from jank objects into native types and vice versa
  6. Explicit casts

You can read about all of these in detail on the jank blog here.

Next month

We’re one month into the quarter and I’m pleased with the progress so far. However, there’s a lot remaining work to do. I still need to tackle free/static function calls, member access, member function calls, operators, dynamic allocations, complex type support, and automatic destructors for locals with the same guarantees C++ provides. On top of that, I need to make sure we can process C++ headers in a portable way. This will definitely keep me busy for the quarter! Stay tuned for my next update in a month.


Permalink

Newsletter 63 – 05/2025

Featured

Microservices Are a Tax Your Startup Probably Can’t Afford – Microservices architecture are well known for the amount of platform engineering required to extract the best from it, if you’re a small company, probably you’ll not have time and tooling to pay the taxes of such complex architecture.

After months of coding with LLMs, I’m going back to using my brain – There’s a lot of success histories around about the usage of LLMs to code, this one is the opposite, talks about how LLMs struggle to code properly when code get complex and the amount of business rules increase. At the end, the engineer always need to take action over it.

That’s How We’ve Always Done Things Around Here – It’s not very uncommon to hear “That’s how we’ve always done things around here” on companies, especially when you’re a newcomer, sometimes this innocent statement can led to interesting things.

A leap year check in three instructions – Leap year check are a famous programming exercise that most students do in the university, this approach to solve the problem is complex but the reason why it works it’s a journey through many computer science and engineering concepts.

How to provide feedback on documents. – Reading technical documents are part of most IT workers nowadays. This post gives nice tips about how to read them and provide feedbacks.

MISC

Graceful Shutdown in Go: Practical Patterns – Graceful shutdown is a important concept not only on Go but on every programming language, that most developers don’t know how to do it correctly. This post explains howw to do it in Golang.

Differential Coverage for Debugging – Differential Coverage is a interesting technique to debug code, based on comparing coverage files.

Elixir is not owned by Big Tech – Many open source project including some programming languages (like Golang) are backed by Big Tech companies, Elixir in the other hand, keep being maintained by a group of developers from small companies, this fact comes with pros and cons.

One line of code that did cost $8,000 – Who never forgot to set up cost alert on the cloud provider and was surprise by the bill in the end of the month?

Stability by Design – Clojure libraries are well known to be very stable, some libraries even stay untouchable for many years because are “complete”. This post tries to explain why this happen and what is special on Clojure community.

This 150-Line Go Script Is Actually a Full-On Load Balancer – Code real world things is one of the best way to learn concepts and how things work. This post gives a brief code to understand how load balancers work.

Permalink

Beyond the If Pattern

by Laurence Chen

In my work at Gaiwan, there was a piece of code with poor quality that always felt like a thorn in my side. For a long time, I couldn’t come up with a better way to handle it.

The code was a Nested If. Each step-* is a side-effect operation, and each handler records a log.

(if (step-a ctx)
  (if (step-b ctx)
    (if (step-c ctx)
        (do-something)
      (handle-c ctx))
    (handle-b ctx))
  (handle-a ctx))

Writing code like this is genuinely painful. The pain stems from several causes:

  1. These if forms are tightly packed together. With this layout, if any of the (handle...) branches get slightly longer, the whole block becomes very long and hard to read.
  2. It’s easy to forget to write a handler branch.
  3. It’s also not easy to modify later.

Right after writing it, I immediately pulled a colleague aside to discuss: there must be a better way to write this, right? Back then, the first idea that came to mind was using a Monad. Maybe a Monad could make this code easier to read, but considering long-term maintenance, we were worried that the abstraction might become a barrier to understanding, so we didn’t adopt it.

some->

In fact, Clojure already has syntax similar to Monads that can handle this type of problem, so I thought of some->. In the following example, if any step returns nil, the whole pipeline short-circuits.

(some-> context
        step1
        step2
        step3)

However, this still doesn’t meet my needs, because if any step fails, I need to handle and log it individually. That way, when this code runs in a production environment, I can quickly pinpoint exactly which step the error occurred in.

ok->

I found a feasible solution in an article by Dave Liepmann. In it, he provides a macro: ok->, which can be considered an enhanced version of some->.

If a step fails, as long as it writes an :error, the pipeline short-circuits and the error info is preserved in the context. In other words, I can choose a handler based on the value of :error.

ok-> is already usable. If there’s anything unsatisfactory about it, it would be the Locality of Behavior—it requires steps and handlers to be written in separate locations.

(defmacro ok->
  "Like `some->` but for `:error` keys instead of `nil` values:
  When expr does not contain an `:error` key, threads it into the first
  form (via ->), and when that result does not contain an `:error` key, through
  the next etc"
  [expr & forms]
  (let [g (gensym)
        steps (map (fn [step] `(if (contains? ~g :error)
                                ~g
                                (-> ~g ~step)))
                   forms)]
    `(let [~g ~expr
           ~@(interleave (repeat g) (butlast steps))]
       ~(if (empty? steps)
          g
          (last steps)))))

ensure->

Inspired by ok->, I developed ensure->.

(defmacro ensure->
  "Threaded validation pipeline with short-circuiting and error handling.

  Takes a context and a series of condition/handler pairs followed by a final
  expression to evaluate if all conditions pass.

  Each condition is evaluated in order. If the condition returns truthy, the
  pipeline proceeds. If it returns falsey, the corresponding handler is invoked
  with the original context, and the pipeline terminates immediately.

  This is useful when:
  - Each step has a specific failure case to handle
  - You want linear syntax instead of deeply nested `if`
  - You want to maintain clear, stepwise semantics in validations or workflows"
  [context & steps]
  (let [pairs (partition 2 (butlast steps))
        final-fn (last steps)]
    (reduce
     (fn [acc [step-fn handler-fn]]
       `(let [ctx# ~context]
          (if (~step-fn ctx#)
            ~acc
            (~handler-fn ctx#))))
     `(~final-fn ~context)
     (reverse pairs))))

Usage:

(ensure-> ctx
          step-a handle-a
          step-b handle-b
          step-c handle-c
          do-something)

This expands to:

(if (step-a ctx)
  (if (step-b ctx)
    (if (step-c ctx)
      (do-something ctx)
      (handle-c ctx))
    (handle-b ctx))
  (handle-a ctx))

The semantics of ensure-> is: “verify a sequence of conditions, and on failure, short-circuit and handle it accordingly.” Since this pattern is commonly used, abstracting it into a reusable macro makes it easier for developers to understand. More importantly, it elevates the semantics from mere “control flow” to “validation and error handling.”

By rewriting the original nested if using the ensure-> macro, the code becomes easier to read and maintain thanks to the linear structure. It avoids introducing complex abstractions—just simple syntactic sugar. Additionally, the name ensure-> itself clearly communicates the intent of the pattern.

cond not

While using a macro like ensure-> makes the intent explicit, is there a non-macro alternative? In fact, ensure-> can be replaced by the cond not pattern.

(def ctx ...)

(cond
  (not (step-a ctx)) (handle-a ctx)
  (not (step-b ctx)) (handle-b ctx)
  (not (step-c ctx)) (handle-c ctx)
  :else
  (do-something ctx))

The if pattern is a code smell

Come to think of it, the nested if pattern is fairly common. These if structures often combine into higher-level semantics. But when the only tool for expressing that is if, the semantics are stuck at the branching control structure level, making the code harder to understand and modify.

Here are some idiomatic alternatives that express richer semantics and avoid overly verbose if structures.

Nested “conditionally update”

Problem: You want to update a hashmap hm only when a condition is met.

(if c
    (assoc hm :x y)
    hm)

Solution:

(cond-> hm
   c (assoc :x y))

When there are many such conditions, using cond-> significantly improves readability by converting nested logic into a linear form.

Preventing update-in from breaking on nil

Problem:

We need to perform an operation on a certain path inside a HashMap hm.

  • If the path is empty, we insert an empty vector and then insert val into it.
  • If the path already has a vector, we just conj the value to it.

The naive way to write it might be:

(if (nil? (get-in hm path))
    (assoc-in hm path [val])
    (update-in hm path conj val))

Solution:

(update-in hm path (fnil conj []) val)

Enforcing Atom invariants

Problem: An Atom must always hold a positive number.

(def a
  (atom 3))

(swap! a (fn [n]
           (if (pos? (dec n))
             (dec n)
             (throw (ex-info "content must be positive"
                             {:a 1})))))

Solution:

(def a
  (atom 3 :validator pos?))

(swap! a dec)

Conclusion

When we write nested ifs layer by layer, it’s often because the language is leading us to express things in a certain way. What we really want to express is: “this is a series of validations—stop on failure, otherwise keep going.” But the language doesn’t give us the best tools to do that.

That’s why the Clojure community has developed many idioms—like cond not, cond->, fnil, :validator. These aren’t just code-reduction macros; they make your intent clearer.

The ultimate goal of programming has never been just about controlling flow—it’s about designing meaning.

So the next time you’re knee-deep in an if jungle, stop and ask yourself: What are you really trying to express? Then let the syntax serve your semantics—not the other way around.

>

Permalink

Too many degrees of freedom

I love conference talks. I believe that YouTube has made me a better programmer over the last 17 years. I’ll often turn one on while I’m doing chores. I’ll learn something and sometimes even be inspired to write about it. Like this one.

The talk I was watching was You’re Doing Exceptions Wrong by Matt Burke. In it, Burke presents some rather compelling advice on how to design your exception throwing and handling to make your code more robust and easier to work with. I quite agree with a lot of what he said, though I think there’s plenty of room for subtleties and context-dependent decisions. You can’t cover everything in a one-hour talk.

What struck me, though, was how easy exceptions are to get wrong. He goes over many examples where poor use of exceptions led to problems. Sometimes people caught exceptions and ignored them. Sometimes they threw the wrong exception. Etc., etc. There was a lot of advice to digest and apply. And you could still get it wrong.

It got me thinking about how many degrees of freedom there are when dealing with exceptions. What class of exception you choose? Should you throw? How much code you wrap your try/catch in. Do you catch at all? How specific do you make your catch statement’s selector? How do you rethrow? Do you need a finally? All of these were subtle and non-mechanical decisions. That’s a lot of cognitive work going on.

It reminds me a lot of the advice in Java Concurrency in Practice. It’s a great book. The advice is solid. I learned a ton about Java when reading that book. But it’s impossible to apply correctly all the time. It is 432 pages of dos and don’ts. Java’s design defaults to a sequential model of computation—one thread. To switch to a concurrent model, you have to start using a whole set of new conventions. You need to properly use the volatile and synchronized keywords, choose “thread safe” classes, and master a whole set of concurrency primitives. There are too many degrees of freedom.

Clojure solved this by reversing the situation: Make concurrency the default. What does that look like? Immutable data structures and concurrency primitives with simple contracts. You can still do sequential programming for those inner loops where you need it. But it’s more cumbersome. In short, Clojure chose the more general case (multiple threads) and constrained the degrees of freedom by choosing the right defaults.

I wonder if there isn’t something like that for exceptions. What would that look like? It would be about choosing the right, general-case default, reduce the number of decisions that have to be made, and making the few decisions left easy to get right. (Aside: This is something Clojure does particularly well.)

If we assume the default is the general case (every function is broken :), then we

The first thing I would do is to come up with a short list of scenarios that the exception can capture. Here’s a first stab:

  • IllegalArgumentException — somebody passed me an invalid argument

  • UnexpectedReturnValueException — I called a function and got something back I wasn’t ready for

  • NoPossibleAnswerException — I was called with valid arguments but I can’t fulfill the contract

The main idea behind the first two is try to distinguish the source of bad values. The ubiquitous NullPointerException tells you you got a null, but not whether it was from an argument or a return value. I’m not as concerned about whether it was a null or some other invalid value (such as 0 for division) as much as where the bad value came from. These exceptions would require you to provide the value.

NoPossibleAnswerException is for situations where you want to throw an exception because you can’t answer. For example, if I ask for the configuration key “HTTP_PORT” but it’s not set, I may want to throw an exception instead of returning null. This is the exception for that case.

There are still other exceptions that might come up. I’m thinking about FileNotFound or TimeoutExceptions. I wonder where those go.

There’s another dimension by which we should slice the exceptions: Whether or not they should be retried. For example, if there’s a timeout on an http get request, go ahead and retry. But if you divide by zero, retrying is useless. If there’s one piece of information I’d love to have available programmatically, it’s whether the thrower thinks retrying is a good idea.

With these few pieces, you can build some primitives that help localize the errors:

(defn divide [num denom]
  (assert-argument denom (not (zero? denom)))
  (/ num denom))

assert-argument is a macro that throws IllegalArgumentExceptions if the condition is not true. It can use the form to report good error messages, including the name of the argument, its value, and the condition that failed. It helps document your assumptions about the arguments you will receive.

Similarly, you can document the assumptions about the return values you will get with assert-return.

Unfortunately, most existing libraries (certainly those in Java) do not follow these conventions. What has worked best for me is to build something like an Anti-Corruption Layer (as in Domain Driven Design). The anti-corruption layer wraps the library and converts the library’s conventions into the conventions of my codebase. It’s a place to say “this is how we use this library” and “this is what we expect this library to do”. It seems redundant at first. Why wrap each library function you call in another function that just calls the library function? Well, it’s to constrain and centralize the assumptions you’re making about the library. Instead of having workarounds for the library spread throughout the codebase, you centralize them and standardize them to the anti-corruption layer.

Before I conclude, I want to mention a different yet similar approach. Erlang is famous for its “let it crash and retry” strategy for handling errors. It’s similar in that Erlang assumes anything can fail (the default is something will go wrong). And the default strategy is to reset the state and try again. This often works, especially with stateful systems. I think it’s a great default.

The problem I have encountered with it is that you do need a certain level of correctness for retries to be effective. If your code doesn’t handle the null you got, retrying it is not going to fix that. You would like to surface bugs early (hopefully during development) so you can fix them. In those cases, you don’t want to hide the problem with a retry. However, we all know that bugs are inevitable. So being protective and having a decent default strategy is prudent. Retry seems to be pretty good to me as a default. So it seems we may want different behavior in production from during development.

In development, crash early so we can surface the errors. In production, do your best to continue without errors. For instance, if the product recommendations don’t load on your product page for a new tea kettle, you should still load the page, just without recommendations. That way, the user can still buy their tea kettle. But don’t do that during development because if you break the recommendation engine, you want to know.

The tension between these two behaviors shows that there are still significant decision points a programmer will need to make. The key is to make the decision easy to get right. That means reducing the number of options and making each option easier to do right. We want to standardize and reduce boilerplate.

Let’s conclude. Exception throwing and handling has a lot of degrees of freedom—too many to get right all the time. It’s ironic because the error reporting and handling will have errors! We can apply Clojure’s design principles of using the general case as the default. In this case, it’s that the function you’re calling can’t be trusted fully, and the caller of the current function can’t be trusted. You have to check your arguments and the return values. But checking them is a lot of work and easy to get wrong. We need better primitives to make it easy to get right. I proposed a systematic evaluation of the kinds of errors you want to throw, and making those easy to throw. I talked about an anti-corruption layer to enforce conventions when using third-party libraries. And I talked about wanting different behavior in development and production. I don’t think I got it all right. So please let me know what your policies are in your software. And I’m also interested in knowing in what other areas (besides exceptions) you apply the policy of “general case is the default”.

Permalink

How do you prototype a nice language?

How do you prototype a nice language?

I’ve spent the past month prototyping my codeCAD language. However, while I’ve made tons of zero-to-one-type progress (an EBNF grammar, parser, function definitions, evaluation, and numeric solving!), the current possible demos are all terribly underwhelming — think the game programmer’s “triangle with color gradient” or the hardware engineer’s “PCB with a single blinking LED”.

However, even once I’m further along I suspect the demos will be fairly underwhelming — I’m not one-shot prompting my way to production G-code or building a slick augmented reality, Minority Report UI.

Rather, I’m after a particular kind of software hygge: Loads instantly, doesn’t crash, and fits nicely in the hand.

The objective is a feeling, and there’s no point trying to convince people — either the software exists and evokes the feeling, or it doesn’t.

Pitching it feels as nonsensical to me as pitching the deliciousness of an unbaked cake (which is why you haven’t heard about my crunchy tiramisu).

Unfortunately, this perspective makes prototyping tricky: How much baking is required to test an idea?

For example: One idea I’m exploring is “bidirectional editing”, so geometry can be manipulated using either:

  • a purpose-built graphical UI, or
  • the textual codeCAD language

If you graphically drag a point around, the coordinates in the source code should automatically update. If you edit the source code, the graphical UI should automatically update.

A simple way to test this idea is to throw a <textarea> in the UI that displays the corresponding source code. But to me, that feels terrible because I never want to be coding in some janky, in-browser <textarea> — I want to be working with source code in Emacs, with all of my familiar key bindings, color schemes, autocomplete, and decades of cozy practice.

That’s the core appeal of a textual programming language.

But doing this properly is an absolute boatload of work:

  • How does the system rewrite source code? Is it mediated by files on disk with reload on save? How do the editor and UI stay in sync and avoid clobbering each other’s unsaved changes? Maybe we need an LSP server?
  • The language interpreter needs to preserve comments and flow them through, even when the UI makes edits to the code.
  • What about whitespace / pretty-printing?

How much of this needs to be built to evaluate whether bidirectional editing “fits nicely in the hand”?

It’s a fine line balancing between the reasonable “this is a prototype, we can fix the awkward parts later” and the tautological “the idea is good if we ignore all the bad stuff”.

Language design resources

While I’ve toyed with some programming language-ish projects before (a microcontroller configuration solver and relational spreadsheet), this is my first time building a language starting from a grammar and implementing stuff like binding, function resolution, and evaluation.

While there’s tons of 101-level resources for “building your first LISP” and evaluating arithmetic expressions like “1 + 2 / 5”, I haven’t found as much that gets in the fiddly details of more complex language semantics.

The most useful book I’ve found is Nystrom’s Crafting Interpreters.

I roughly followed its approach to building a lil’ tree-walking interpreter, but with Instaparse for parsing and some of the data format and processing machinery from Clojure tools.analyzer.

So far I’ve punted on doing proper (whitespace and comment preserving) unparsing in favor of brute force canonicalizing abstract-syntax-tree -> string rendering, but I expect I’ll need to flesh that out along with LSP support sooner rather than later.

I considered replacing Instaparse with Tree-sitter to leg into the latter’s better performance and potential ecosystem benefits (the “universal formatting engine” Topiary; Combobulate structured editing).

However, I decided against it for now as I feel like it’s worth trying to grapple with the challenges myself using a familiar tool (Clojure), rather than picking up frameworks and hoping I can mold them into the to-be-determined experience I’m trying to provide.

I have been reading around to learn about unified approaches for building a language with first-party support for editor tooling — where the LSP engine isn’t a from-scratch duplication of the compiler/interpreter’s analyzer, but is rather integrated as part of a single codebase.

(Aside: While a language implementation and editor tooling may make different trade offs around, e.g., throughput, latency, and tolerance of malformed inputs, I feel like a unified approach is more sustainable for single-author art project language.)

Some prior art I’ve seen there:

  • the Gleam language, which is written in Rust and has first-party LSP support. If I’m reading the source correctly, it tracks comments separately from the abstract syntax tree and then merges them back together when pretty printing.

  • Lady Deirdre is a framework that helps you develop front-end code analysis tools, such as code editor language extensions, programming language compilers and interpreters, and even new code editors.”

  • a formal / Haskell-ish take on parsing and unparsing with lenses

  • the .NET compiler platform (“Roslyn”) stores trivia (comments, etc.) on tokens in the syntax tree.

Anyway, if you know of any programming language implementations (or bidirectional editing work) that I should study, please let me know!

Misc. stuff

Permalink

Simplicity to scale systems and transform software engineering

At Nu México Meetups #12, we explored how simplicity is a core strategic principle in building scalable software—and why it’s essential to deliver reliable and accessible financial products to millions of people.

Our guest for this session was Jordan Miller, a technology and education specialist focused on people, programming, and pedagogy. With deep expertise in Clojure and Datomic, Jordan is the co-author of Clojure Brain Teasers, co-founder of Clojure Camp, and host of the podcast Lost In Lambduhhs. In her talk, she shared valuable insights into how simplicity—when applied deliberately—can become a powerful strategic advantage for engineering teams, especially in organizations that need to scale with responsibility and purpose.

In this post, we walk through the main concepts covered in the talk: the traps of accidental complexity, the three levels where complexity manifests (organizational, team, and individual), the difference between something being simple and easy, and how tools like Clojure and Datomic help us build sustainable, auditable, and resilient systems.

Why simplicity is both a technical and human superpower

Simplicity is not the same as ease. While “easy” is subjective and depends on one’s experience, “simple” is objective: it means having fewer dependencies and greater conceptual clarity. In software development, that translates to reducing the cognitive load for those who build, maintain, and evolve systems.

At Nubank, simplicity is intentional. It’s the outcome of structural decisions that favor clarity over immediate convenience. A simple system is easier to understand, debug, extend—and, most importantly, to trust.

As humans, we can only handle a limited number of concepts at once. That’s why systems overloaded with layers of abstraction, tightly coupled dependencies, or mutable states tend to become harder to reason about and maintain. When these layers grow unchecked, they create what we call technical debt: the growing cost of understanding and maintaining systems that no longer scale well.

Where complexity shows up: three levels that require attention

System complexity doesn’t only live in code—it emerges at multiple levels within an organization:

1. Organizational

At a macro level, complexity often stems from team misalignment, fragmented technology choices, or isolated decision-making that impairs collaboration. To avoid this, Nubank has adopted a unified technology stack with Clojure, ClojureScript, and Datomic—enabling better communication across backend and frontend teams and greater mobility between them.

We also embrace autonomy with accountability, one of our core values, which requires both trust and technical clarity.

2. Team

Within engineering teams, simplicity is supported by collaborative design, shared reviews, and collective understanding—long before any code is written. This approach breaks down knowledge silos and reinforces familiarity with the system, which is essential for sustainable growth.

3. Individual

At a personal level, simplicity means letting go of ego, being open to feedback, and feeling safe to make mistakes. It also means recognizing the “zone of proximal development,” where learning and collaboration thrive.

This mindset allows engineers to grow without fear of irreversible impact—because the systems we build are intentionally designed to be auditable and resilient.

Solve the right problem before building the solution

Another pillar discussed during the Meetup was structured problem-solving. Complexity often grows when we jump into solutions without fully understanding the underlying issue.

Here, we apply an approach inspired by Socratic thinking and the scientific method: we ask questions, form hypotheses, run tests, and document our findings. We use decision matrices to weigh the trade-offs of different approaches—an investment that pays off when building long-lasting solutions.

But what makes a problem “good”? It needs to have explicit criteria, be relevant, and pose a meaningful obstacle on the path to progress. Well-defined problems help avoid unnecessary implementations and create clarity for everyone involved.

Clojure and Datomic in Nubank’s technical culture

Nubank’s choice of functional programming languages like Clojure and immutable databases like Datomic is not incidental. These tools align closely with our engineering vision: to build systems that prioritize clarity, auditability, security, and continuous evolution.

Datomic, for example, gives us full visibility into the historical state of our data—complete with native audit trails and without unpredictable side effects. This is especially critical in financial services, where trust and traceability are non-negotiable.

Beyond using these tools, Nubank also actively supports the open source projects behind them. Since acquiring Cognitect in 2020, we’ve continued investing in the Clojure ecosystem and the community that powers it.

Simplicity as a competitive advantage

Today, Nubank serves more than 118 million customers through a platform that’s highly reliable, scalable, and secure. This is only possible because we embrace simplicity at every level—from backend systems to how we collaborate across teams.

For us, simplicity isn’t just a technical choice. It’s a product and cultural philosophy. It allows us to innovate safely, grow responsibly, and most of all, keep customers at the center of the experience. As we like to say: the best systems aren’t the most complex—they’re the most clear, predictable, and human.

If you’re curious about functional programming, data-oriented development, or how Nubank builds at scale, check out future Meetups and posts here on the Building Nubank blog. And if you’d like to dive deeper, explore the book Clojure Brain Teasers or join our community learning group. Because learning together is also a way to keep things simple.

The post Simplicity to scale systems and transform software engineering appeared first on Building Nubank.

Permalink

Clojure 1.12.1

Clojure 1.12.1 is now available! Find download and usage information on the Downloads page.

  • CLJ-2899 - Revert change in semantics of qualified symbol in invocation position if field and method of same name

  • CLJ-2898 - Objects that are both IFn and FunctionalInterface unnecessarily get converted to FI

  • CLJ-2888 - gen-class - did not support new array class symbol syntax

  • CLJ-2886 - add-libs - send only procurer keys to tool invocation

  • CLJ-2906 - Add missing :added metadata to 1.12 functions

Permalink

Build and Deploy Web Apps With Clojure and FLy.io

This post walks through a small web development project using Clojure, covering everything from building the app to packaging and deploying it. It’s a collection of insights and tips I’ve learned from building my Clojure side projects but presented in a more structured format.

As the title suggests, we’ll be deploying the app to Fly.io. It’s a service that allows you to deploy apps packaged as Docker images on lightweight virtual machines.[1] My experience with it has been good, it’s easy to use and quick to set up. One downside of Fly is that it doesn’t have a free tier, but if you don’t plan on leaving the app deployed, it barely costs anything.

This isn’t a tutorial on Clojure, so I’ll assume you already have some familiarity with the language as well as some of its libraries.[2]

Project Setup

In this post, we’ll be building a barebones bookmarks manager for the demo app. Users can log in using basic authentication, view all bookmarks, and create a new bookmark. It’ll be a traditional multi-page web app and the data will be stored in a SQLite database.

Here’s an overview of the project’s starting directory structure:

.
├── dev
│   └── user.clj
├── resources
│   └── config.edn
├── src
│   └── acme
│       └── main.clj
└── deps.edn

And the libraries we’re going to use. If you have some Clojure experience or have used Kit, you’re probably already familiar with all the libraries listed below.[3]

;; deps.edn
{:paths ["src" "resources"]
 :deps {org.clojure/clojure               {:mvn/version "1.12.0"}
        aero/aero                         {:mvn/version "1.1.6"}
        integrant/integrant               {:mvn/version "0.11.0"}
        ring/ring-jetty-adapter           {:mvn/version "1.12.2"}
        metosin/reitit-ring               {:mvn/version "0.7.2"}
        com.github.seancorfield/next.jdbc {:mvn/version "1.3.939"}
        org.xerial/sqlite-jdbc            {:mvn/version "3.46.1.0"}
        hiccup/hiccup                     {:mvn/version "2.0.0-RC3"}}
 :aliases
 {:dev {:extra-paths ["dev"]
        :extra-deps  {nrepl/nrepl    {:mvn/version "1.3.0"}
                      integrant/repl {:mvn/version "0.3.3"}}
        :main-opts   ["-m" "nrepl.cmdline" "--interactive" "--color"]}}}

I use Aero and Integrant for my system configuration (more on this in the next section), Ring with the Jetty adaptor for the web server, Reitit for routing, next.jdbc for database interaction, and Hiccup for rendering HTML. From what I’ve seen, this is a popular “library combination” for building web apps in Clojure.[4]

The user namespace in dev/user.clj contains helper functions from Integrant-repl to start, stop, and restart the Integrant system.

;; dev/user.clj
(ns user
  (:require
   [acme.main :as main]
   [clojure.tools.namespace.repl :as repl]
   [integrant.core :as ig]
   [integrant.repl :refer [set-prep! go halt reset reset-all]]))

(set-prep!
 (fn []
   (ig/expand (main/read-config)))) ;; we'll implement this soon

(repl/set-refresh-dirs "src" "resources")

(comment
  (go)
  (halt)
  (reset)
  (reset-all))

Systems and Configuration

If you’re new to Integrant or other dependency injection libraries like Component, I’d suggest reading “How to Structure a Clojure Web”. It’s a great explanation about the reasoning behind these libraries. Like most Clojure apps that use Aero and Integrant, my system configuration lives in a .edn file. I usually name mine as resources/config.edn. Here’s what it looks like:

;; resources/config.edn
{:server
 {:port #long #or [#env PORT 8080]
  :host #or [#env HOST "0.0.0.0"]
  :auth {:username #or [#env AUTH_USER "john.doe@email.com"]
         :password #or [#env AUTH_PASSWORD "password"]}}

 :database
 {:dbtype "sqlite"
  :dbname #or [#env DB_DATABASE "database.db"]}}

In production, most of these values will be set using environment variables. During local development, the app will use the hard-coded default values. We don’t have any sensitive values in our config (e.g., API keys), so it’s fine to commit this file to version control. If there are such values, I usually put them in another file that’s not tracked by version control and include them in the config file using Aero’s #include reader tag.

This config file is then “expanded” into the Integrant system map using the expand-key method:

;; src/acme/main.clj
(ns acme.main
  (:require
   [aero.core :as aero]
   [clojure.java.io :as io]
   [integrant.core :as ig]))

(defn read-config
  []
  {:system/config (aero/read-config (io/resource "config.edn"))})

(defmethod ig/expand-key :system/config
  [_ opts]
  (let [{:keys [server database]} opts]
    {:server/jetty (assoc server :handler (ig/ref :handler/ring))
     :handler/ring {:database (ig/ref :database/sql)
                    :auth     (:auth server)}
     :database/sql database}))

The system map is created in code instead of being in the configuration file. This makes refactoring your system simpler as you only need to change this method while leaving the config file (mostly) untouched.[5]

My current approach to Integrant + Aero config files is mostly inspired by the blog post “Rethinking Config with Aero & Integrant” and Laravel’s configuration. The config file follows a similar structure to Laravel’s config files and contains the app configurations without describing the structure of the system. Previously I had a key for each Integrant component, which led to the config file being littered with #ig/ref and more difficult to refactor.

Also, if you haven’t already, start a REPL and connect to it from your editor. Run clj -M:dev if your editor doesn’t automatically start a REPL. Next, we’ll implement the init-key and halt-key! methods for each of the components:

;; src/acme/main.clj
(ns acme.main
  (:require
   ;; ...
   [acme.handler :as handler]
   [acme.util :as util])
   [next.jdbc :as jdbc]
   [ring.adapter.jetty :as jetty]))
;; ...

(defmethod ig/init-key :server/jetty
  [_ opts]
  (let [{:keys [handler port]} opts
        jetty-opts (-> opts (dissoc :handler :auth) (assoc :join? false))
        server     (jetty/run-jetty handler jetty-opts)]
    (println "Server started on port " port)
    server))

(defmethod ig/halt-key! :server/jetty
  [_ server]
  (.stop server))

(defmethod ig/init-key :handler/ring
  [_ opts]
  (handler/handler opts))

(defmethod ig/init-key :database/sql
  [_ opts]
  (let [datasource (jdbc/get-datasource opts)]
    (util/setup-db datasource)
    datasource))

The setup-db function creates the required tables in the database if they don’t exist yet. This works fine for database migrations in small projects like this demo app, but for larger projects, consider using libraries such as Migratus (my preferred library) or Ragtime.

;; src/acme/util.clj
(ns acme.util 
  (:require
   [next.jdbc :as jdbc]))

(defn setup-db
  [db]
  (jdbc/execute-one!
   db
   ["create table if not exists bookmarks (
       bookmark_id text primary key not null,
       url text not null,
       created_at datetime default (unixepoch()) not null
     )"]))

For the server handler, let’s start with a simple function that returns a “hi world” string.

;; src/acme/handler.clj
(ns acme.handler
  (:require
   [ring.util.response :as res]))

(defn handler
  [_opts]
  (fn [req]
    (res/response "hi world")))

Now all the components are implemented. We can check if the system is working properly by evaluating (reset) in the user namespace. This will reload your files and restart the system. You should see this message printed in your REPL:

:reloading (acme.util acme.handler acme.main)
Server started on port  8080
:resumed

If we send a request to http://localhost:8080/, we should get “hi world” as the response:

$ curl localhost:8080/
hi world

Nice! The system is working correctly. In the next section, we’ll implement routing and our business logic handlers.

Routing, Middleware, and Route Handlers

First, let’s set up a ring handler and router using Reitit. We only have one route, the index / route that’ll handle both GET and POST requests.

;; src/acme/handler.clj
(ns acme.handler
  (:require
   [reitit.ring :as ring]))

(def routes
  [["/" {:get  index-page
         :post index-action}]])

(defn handler
  [opts]
  (ring/ring-handler
   (ring/router routes)
   (ring/routes
    (ring/redirect-trailing-slash-handler)
    (ring/create-resource-handler {:path "/"})
    (ring/create-default-handler))))

We’re including some useful middleware:

  • redirect-trailing-slash-handler to resolve routes with trailing slashes,
  • create-resource-handler to serve static files, and
  • create-default-handler to handle common 40x responses.

Implementing the Middlewares

If you remember the :handler/ring from earlier, you’ll notice that it has two dependencies, database and auth. Currently, they’re inaccessible to our route handlers. To fix this, we can inject these components into the Ring request map using a middleware function.

;; src/acme/handler.clj
;; ...

(defn components-middleware
  [components]
  (let [{:keys [database auth]} components]
    (fn [handler]
      (fn [req]
        (handler (assoc req
                        :db database
                        :auth auth))))))
;; ...

The components-middleware function takes in a map of components and creates a middleware function that “assocs” each component into the request map.[6] If you have more components such as a Redis cache or a mail service, you can add them here.

We’ll also need a middleware to handle HTTP basic authentication.[7] This middleware will check if the username and password from the request map matche the values in the auth map injected by components-middleware. If they match, then the request is authenticated and the user can view the site.

;; src/acme/handler.clj
(ns acme.handler
  (:require
   ;; ...
   [acme.util :as util]
   [ring.util.response :as res]))
;; ...

(defn wrap-basic-auth
  [handler]
  (fn [req]
    (let [{:keys [headers auth]} req
          {:keys [username password]} auth
          authorization (get headers "authorization")
          correct-creds (str "Basic " (util/base64-encode
                                       (format "%s:%s" username password)))]
      (if (and authorization (= correct-creds authorization))
        (handler req)
        (-> (res/response "Access Denied")
            (res/status 401)
            (res/header "WWW-Authenticate" "Basic realm=protected"))))))
;; ...

A nice feature of Clojure is that interop with the host language is easy. The base64-encode function is just a thin wrapper over Java’s Base64.Encoder:

;; src/acme/util.clj
(ns acme.util
   ;; ...
  (:import java.util.Base64))

(defn base64-encode
  [s]
  (.encodeToString (Base64/getEncoder) (.getBytes s)))

Finally, we need to add them to the router. Since we’ll be handling form requests later, we’ll also bring in Ring’s wrap-params middleware.

;; src/acme/handler.clj
(ns acme.handler
  (:require
   ;; ...
   [ring.middleware.params :refer [wrap-params]]))
;; ...

(defn handler
  [opts]
  (ring/ring-handler
   ;; ...
   {:middleware [(components-middleware opts)
                 wrap-basic-auth
                 wrap-params]}))

Implementing the Route Handlers

We now have everything we need to implement the route handlers or the business logic of the app. First, we’ll implement the index-page function which renders a page that:

  1. Shows all of the user’s bookmarks in the database, and
  2. Shows a form that allows the user to insert new bookmarks into the database
;; src/acme/handler.clj
(ns acme.handler
  (:require
   ;; ...
   [next.jdbc :as jdbc]
   [next.jdbc.sql :as sql]))
;; ...

(defn template
  [bookmarks]
  [:html
   [:head
    [:meta {:charset "utf-8"
            :name    "viewport"
            :content "width=device-width, initial-scale=1.0"}]]
   [:body
    [:h1 "bookmarks"]
    [:form {:method "POST"}
     [:div
      [:label {:for "url"} "url "]
      [:input#url {:name "url"
                   :type "url"
                   :required true
                   :placeholer "https://en.wikipedia.org/"}]]
     [:button "submit"]]
    [:p "your bookmarks:"]
    [:ul
     (if (empty? bookmarks)
       [:li "you don't have any bookmarks"]
       (map
        (fn [{:keys [url]}]
          [:li
           [:a {:href url} url]])
        bookmarks))]]])

(defn index-page
  [req]
  (try
    (let [bookmarks (sql/query (:db req)
                               ["select * from bookmarks"]
                               jdbc/unqualified-snake-kebab-opts)]
      (util/render (template bookmarks)))
    (catch Exception e
      (util/server-error e))))
;; ...

Database queries can sometimes throw exceptions, so it’s good to wrap them in a try-catch block. I’ll also introduce some helper functions:

;; src/acme/util.clj
(ns acme.util
  (:require
   ;; ...
   [hiccup2.core :as h]
   [ring.util.response :as res])
  (:import java.util.Base64))
;; ...

(defn preprend-doctype
  [s]
  (str "<!doctype html>" s))

(defn render
  [hiccup]
  (-> hiccup h/html str preprend-doctype res/response (res/content-type "text/html")))

(defn server-error
  [e]
  (println "Caught exception: " e)
  (-> (res/response "Internal server error")
      (res/status 500)))

render takes a hiccup form and turns it into a ring response, while server-error takes an exception, logs it, and returns a 500 response.

Next, we’ll implement the index-action function:

;; src/acme/handler.clj
;; ...

(defn index-action
  [req]
  (try
    (let [{:keys [db form-params]} req
          value (get form-params "url")]
      (sql/insert! db :bookmarks {:bookmark_id (random-uuid) :url value})
      (res/redirect "/" 303))
    (catch Exception e
      (util/server-error e))))
;; ...

This is an implementation of a typical post/redirect/get pattern. We get the value from the URL form field, insert a new row in the database with that value, and redirect back to the index page. Again, we’re using a try-catch block to handle possible exceptions from the database query.

That should be all of the code for the controllers. If you reload your REPL and go to http://localhost:8080, you should see something that looks like this after logging in:

Screnshot of the app

The last thing we need to do is to update the main function to start the system:

;; src/acme/main.clj
;; ...

(defn -main [& _]
  (-> (read-config) ig/expand ig/init))

Now, you should be able to run the app using clj -M -m acme.main. That’s all the code needed for the app. In the next section, we’ll package the app into a Docker image to deploy to Fly.

Packaging the App

While there are many ways to package a Clojure app, Fly.io specifically requires a Docker image. There are two approaches to doing this:

  1. Build an uberjar and run it using Java in the container, or
  2. Load the source code and run it using Clojure in the container

Both are valid approaches. I prefer the first since its only dependency is the JVM. We’ll use the tools.build library to build the uberjar. Check out the official guide for more information on building Clojure programs. Since it’s a library, to use it we can add it to our deps.edn file with an alias:

;; deps.edn
{;; ...
 :aliases
 {;; ...
  :build {:extra-deps {io.github.clojure/tools.build 
                       {:git/tag "v0.10.5" :git/sha "2a21b7a"}}
          :ns-default build}}}

Tools.build expects a build.clj file in the root of the project directory, so we’ll need to create that file. This file contains the instructions to build artefacts, which in our case is a single uberjar. There are many great examples of build.clj files on the web, including from the official documentation. For now, you can copy+paste this file into your project.

;; build.clj
(ns build
  (:require
   [clojure.tools.build.api :as b]))

(def basis (delay (b/create-basis {:project "deps.edn"})))
(def src-dirs ["src" "resources"])
(def class-dir "target/classes")

(defn uber
  [_]
  (println "Cleaning build directory...")
  (b/delete {:path "target"})

  (println "Copying files...")
  (b/copy-dir {:src-dirs   src-dirs
               :target-dir class-dir})

  (println "Compiling Clojure...")
  (b/compile-clj {:basis      @basis
                  :ns-compile '[acme.main]
                  :class-dir  class-dir})

  (println "Building Uberjar...")
  (b/uber {:basis     @basis
           :class-dir class-dir
           :uber-file "target/standalone.jar"
           :main      'acme.main}))

To build the project, run clj -T:build uber. This will create the uberjar standalone.jar in the target directory. The uber in clj -T:build uber refers to the uber function from build.clj. Since the build system is a Clojure program, you can customise it however you like. If we try to run the uberjar now, we’ll get an error:

# build the uberjar
$ clj -T:build uber
Cleaning build directory...
Copying files...
Compiling Clojure...
Building Uberjar...

# run the uberjar
$ java -jar target/standalone.jar
Error: Could not find or load main class acme.main
Caused by: java.lang.ClassNotFoundException: acme.main

This error occurred because the Main class that is required by Java isn’t built. To fix this, we need to add the :gen-class directive in our main namespace. This will instruct Clojure to create the Main class from the -main function.

;; src/acme/main.clj
(ns acme.main
  ;; ...
  (:gen-class))
;; ...

If you rebuild the project and run java -jar target/standalone.jar again, it should work perfectly. Now that we have a working build script, we can write the Dockerfile:

# Dockerfile
# install additional dependencies here in the base layer
# separate base from build layer so any additional deps installed are cached
FROM clojure:temurin-21-tools-deps-bookworm-slim AS base

FROM base as build
WORKDIR /opt
COPY . .
RUN clj -T:build uber

FROM eclipse-temurin:21-alpine AS prod
COPY --from=build /opt/target/standalone.jar /
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "standalone.jar"]

It’s a multi-stage Dockerfile. We use the official Clojure Docker image as the layer to build the uberjar. Once it’s built, we copy it to a smaller Docker image that only contains the Java runtime.[8] By doing this, we get a smaller container image as well as a faster Docker build time because the layers are better cached.

That should be all for packaging the app. We can move on to the deployment now.

Deploying with Fly.io

First things first, you’ll need to install flyctl, Fly’s CLI tool for interacting with their platform. Create a Fly.io account if you haven’t already. Then run fly auth login to authenticate flyctl with your account.

Next, we’ll need to create a new Fly App:

$ fly app create
? Choose an app name (leave blank to generate one): 
automatically selected personal organization: Ryan Martin
New app created: blue-water-6489

Another way to do this is with the fly launch command, which automates a lot of the app configuration for you. We have some steps to do that are not done by fly launch, so we’ll be configuring the app manually. I also already have a fly.toml file ready that you can straight away copy to your project.

# fly.toml
# replace these with your app and region name
# run `fly platform regions` to get a list of regions
app = 'blue-water-6489' 
primary_region = 'sin'

[env]
  DB_DATABASE = "/data/database.db"

[http_service]
  internal_port = 8080
  force_https = true
  auto_stop_machines = "stop"
  auto_start_machines = true
  min_machines_running = 0

[mounts]
  source = "data"
  destination = "/data"
  initial_sie = 1

[[vm]]
  size = "shared-cpu-1x"
  memory = "512mb"
  cpus = 1
  cpu_kind = "shared"

These are mostly the default configuration values with some additions. Under the [env] section, we’re setting the SQLite database location to /data/database.db. The database.db file itself will be stored in a persistent Fly Volume mounted on the /data directory. This is specified under the [mounts] section. Fly Volumes are similar to regular Docker volumes but are designed for Fly’s micro VMs.

We’ll need to set the AUTH_USER and AUTH_PASSWORD environment variables too, but not through the fly.toml file as these are sensitive values. To securely set these credentials with Fly, we can set them as app secrets. They’re stored encrypted and will be automatically injected into the app at boot time.

$ fly secrets set AUTH_USER=hi@ryanmartin.me AUTH_PASSWORD=not-so-secure-password
Secrets are staged for the first deployment

With this, the configuration is done and we can deploy the app using fly deploy:

$ fly deploy
# ...
Checking DNS configuration for blue-water-6489.fly.dev

Visit your newly deployed app at https://blue-water-6489.fly.dev/

The first deployment will take longer since it’s building the Docker image for the first time. Subsequent deployments should be faster due to the cached image layers. You can click on the link to view the deployed app, or you can also run fly open which will do the same thing. Here’s the app in action:

The app in action

If you made additional changes to the app or fly.toml, you can redeploy the app using the same command, fly deploy. The app is configured to auto stop/start, which helps to cut costs when there’s not a lot of traffic to the site. If you want to take down the deployment, you’ll need to delete the app itself using fly app destroy <your app name>.

Adding a Production REPL

This is an interesting topic in the Clojure community, with varying opinions on whether or not it’s a good idea. Personally I find having a REPL connected to the live app helpful, and I often use it for debugging and running queries on the live database.[9] Since we’re using SQLite, we don’t have a database server we can directly connect to, unlike Postgres or MySQL.

If you’re brave, you can even restart the app directly without redeploying from the REPL. You can easily go wrong with it, which is why some prefer to not use it.

For this project, we’re gonna add a socket REPL. It’s very simple to add (you just need to add a JVM option) and it doesn’t require additional dependencies like nREPL. Let’s update the Dockerfile:

# Dockerfile
# ...
EXPOSE 7888
ENTRYPOINT ["java", "-Dclojure.server.repl={:port 7888 :accept clojure.core.server/repl}", "-jar", "standalone.jar"]

The socket REPL will be listening on port 7888. If we redeploy the app now, the REPL will be started but we won’t be able to connect to it. That’s because we haven’t exposed the service through Fly proxy. We can do this by adding the socket REPL as a service in the [services] section in fly.toml.

However, doing this will also expose the REPL port to the public. This means that anyone can connect to your REPL and possibly mess with your app. Instead, what we want to do is to configure the socket REPL as a private service.

By default, all Fly apps in your organisation live in the same private network. This private network, called 6PN, connects the apps in your organisation through Wireguard tunnels (a VPN) using IPv6. Fly private services aren’t exposed to the public internet but can be reached from this private network. We can then use Wireguard to connect to this private network to reach our socket REPL.

Fly VMs are also configured with the hostname fly-local-6pn, which maps to its 6PN address. This is analogous to localhost, which points to your loopback address 127.0.0.1. To expose a service to 6PN, all we have to do is bind or serve it to fly-local-6pn instead of the usual 0.0.0.0. We have to update the socket REPL options to:

# Dockerfile
# ...
ENTRYPOINT ["java", "-Dclojure.server.repl={:port 7888,:address \"fly-local-6pn\",:accept clojure.core.server/repl}", "-jar", "standalone.jar"]

After redeploying, we can use the fly proxy command to forward the port from the remote server to our local machine.[10]

$ fly proxy 7888:7888
Proxying local port 7888 to remote [blue-water-6489.internal]:7888

In another shell, run:

$ rlwrap nc localhost 7888
user=>

Now we have a REPL connected to the production app! rlwrap is used for readline functionality, e.g. up/down arrow keys, vi bindings. Of course you can also connect to it from your editor.

Deploy with GitHub Actions

If you’re using GitHub, we can also set up automatic deployments on pushes/PRs with GitHub Actions. All you need is to create the workflow file:

# .github/workflows/fly.yaml
name: Fly Deploy
on:
  push:
    branches:
      - main
  workflow_dispatch:

jobs:
  deploy:
    name: Deploy app
    runs-on: ubuntu-latest
    concurrency: deploy-group
    steps:
      - uses: actions/checkout@v4
      - uses: superfly/flyctl-actions/setup-flyctl@master
      - run: flyctl deploy --remote-only
        env:
          FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}

To get this to work, you’ll need to create a deploy token from your app’s dashboard. Then, in your GitHub repo, create a new repository secret called FLY_API_TOKEN with the value of your deploy token. Now, whenever you push to the main branch, this workflow will automatically run and deploy your app. You can also manually run the workflow from GitHub because of the workflow_dispatch option.

End

As always, all the code is available on GitHub. Originally, this post was just about deploying to Fly.io, but along the way I kept adding on more stuff until it essentially became my version of the user manager example app. Anyway, hope this article provided a good view into web development with Clojure. As a bonus, here are some additional resources on deploying Clojure apps:


  1. The way Fly.io works under the hood is pretty clever. Instead of running the container image with a runtime like Docker, the image is unpacked and “loaded” into a VM. See this video explanation for more details. ↩︎

  2. If you’re interested in learning Clojure, my recommendation is to follow the official getting started guide and join the Clojurians Slack. Also, read through this list of introductory resources. ↩︎

  3. Kit was a big influence on me when I first started learning web development in Clojure. I never used it directly, but I did use their library choices and project structure as a base for my own projects. ↩︎

  4. There’s no “Rails” for the Clojure ecosystem (yet?). The prevailing opinion is to build your own “framework” by composing different libraries together. Most of these libraries are stable and are already used in production by big companies, so don’t let this discourage you from doing web development in Clojure! ↩︎

  5. There might be some keys that you add or remove, but the structure of the config file stays the same. ↩︎

  6. “assoc” (associate) is a Clojure slang that means to add or update a key-value pair in a map. ↩︎

  7. For more details on how basic authentication works, check out the specification. ↩︎

  8. Here’s a cool resource I found when researching Java Dockerfiles: WhichJDK. It provides a comprehensive comparison on the different JDKs available and recommendations on which one you should use. ↩︎

  9. Another (non-technically important) argument for live/production REPLs is just because it’s cool. Ever since I read the story about NASA’s programmers debugging a spacecraft through a live REPL, I’ve always wanted to try it at least once. ↩︎

  10. If you encounter errors related to Wireguard when running fly proxy, you can run fly doctor which will hopefully detect issues with your local setup and also suggest fixes for them. ↩︎

Permalink

curiosidades

Image description🚀 Meu Primeiro Projeto em Python: Curiosidades Animais (Após 3 Semanas de Estudo!)
Acabei de concluir meu primeiro projeto em Python e quero compartilhar essa conquista!
🐍 O que fiz:
Desenvolvi um programa interativo de terminal que exibe curiosidades sobre animais (Urso, Gato, Tigre e outros), utilizando:
✅ Estruturas de controle (while, if-elif-else)
✅ Tratamento de inputs do usuário (case-insensitive)
✅ Gerenciamento básico de erros (try-except)
✅ Funções com type hinting (def curiosidade_animal() -> str)

🔗 Confira o código no GitHub:
https://lnkd.in/dkatFxyr

Aprendo bastante lendo livros, artigos e documentações. Mas também peguei algumas dicas com o mestre Gustavo Guanabara Curso em Vídeo

Estou aberto a:
👉 Críticas construtivas
👉 Oportunidades de colaboração
👉 Conversas sobre carreira em desenvolvimento

Permalink

Clojure Deref (May 30, 2025)

Welcome to the Clojure Deref! This is a weekly link/news roundup for the Clojure ecosystem (feed: RSS).

Podcasts, videos, and media

Libraries and Tools

New releases and tools this week:

  • clojure-mcp - Clojure MCP - REPL-Driven Development with AI Assistance

  • inf-clojure 3.3.0 - Basic interaction with a Clojure subprocess

  • scicloj.ml.smiledf - A library to convert between Smile dataframes and tech.ml datasets

  • clojure-lsp 2025.05.27-13.56.57 - Clojure & ClojureScript Language Server (LSP) implementation

  • unused-deps - Find unused deps in a clojure project

  • calva-backseat-driver 0.0.13 - VS Code AI Agent Interactive Programming. Tools for CoPIlot and other assistants. Can also be used as an MCP server.

  • brainflow-java 1.0.006 - Containing only the java brainflow implementation so that it is not necessary to push the brainflow code directly inside the brainfloj clojure wrapper.

  • sweet-array 0.3.0 - Array manipulation library for Clojure with "sweet" array type notation and more safety by static types

  • remus 0.2.5 - Attentive RSS/Atom feed parser for Clojure

  • squint 0.8.148 - Light-weight ClojureScript dialect

  • build-uber-log4j2-handler 2.24.3 - A conflict handler for log4j2 plugins cache files for the tools.build uber task.

  • metamorph.ml 1.2.1 - Machine learning functions for tech.ml.dataset and metamorph

  • clojure-mode 5.20.0 - Emacs support for the Clojure(Script) programming language

Permalink

Is AI Going to Take Our Jobs? A Software Engineer’s Experience with LLMs

Over the past few years, there has been considerable discussion about how AI might replace all our jobs. For a long time, I completely ignored the topic; most of it sounded like pure bu11SH1T. I had already studied neural networks and understood the basics of LLMs, Machine Learning, Deep Learning, Natural Language Processing, and Expert Systems. I had even tested a few generative AI tools, but none of them impressed me; they were buggy, full of hallucinations, and constrained.

Last year (2024), I revisited the subject with more focus. A lot has changed, major improvements were made, but still, nothing close to replacing a real Software Engineer, despite what many headlines suggested at the time.

Since then, I’ve been following the field closely, running experiments and trying to understand how this tech could impact our daily work. That’s how this experiment came to life: I decided to use AI alone to redesign the visual theme of my website (rafaelhs-tech.com). The idea was simple: change only the look and feel of the site. Here’s how it went.

Tech Stack

I originally built my site in 2021 using Angular (version 7 or 8, can’t remember exactly). It was pretty basic, just a few components with a space-themed design.

The Original Version

Old Version

This was the original version. The journey with AI agents began using Claude 3.7. My first prompt went something like this:

“This is an Angular XX application, and I want to change the style to something with a cyberpunk theme. The folders are organized by components, and each component represents a page.”

Claude scanned the project, found the SASS files, and started generating new code… but then it began to hallucinate: it created React files inside an Angular project.

I tried correcting it, but it kept going off track, breaking the app, mixing files, outputting React code again and again. So, I scrapped everything and started over.

This time, I was more specific: I mentioned the Angular version, outlined the folder structure, highlighted key files, and gave a full project overview. The result? Slightly better visually, but still a broken UI. Misaligned pages, inconsistent styles, and worst of all: the code was a mess.

Bad Code

This kind of output was common. For those unfamiliar, mixing HTML, CSS, and JS like this is bad practice in modern web development.

It wasn’t exactly surprising, but it reinforced a key point: these agents still lack a real understanding of good development practices. I kept using Claude 3.7 for a while, but it got frustrating. I’d ask for one thing, and it would return something completely different. After more than 15 prompts just to align a single button, I realized it would’ve been faster to just fix it manually, but I was committed to simulating a non-technical user experience.

I also tested CodeLLM, but it had another issue: it doesn’t retain long-term memory. If the prompt fails or reaches the token limit, continuing in a new thread resets everything, with no memory of past context. That became really annoying.

Common Hallucinations with Claude 3.7:

  • Generated things I didn’t ask for
  • Couldn’t revert changes
  • Deleted everything and regenerated the code in loops

So I switched to Claude 3.5, which behaved more reliably. It followed instructions with better accuracy. I continued tweaking things and eventually got this result:

The Final Version

Final Version

After about 40 prompts, I reached a decent visual result. But the code was still messy: CSS mixed with SASS, JS inline with HTML, weird file names, useless comments... nothing reusable. I decided to try other LLMs like Gemini 2.5, GPT o1, and GPT o4mini. The outcome was the same, frustrating, and each run produced noticeably different results (as expected).

Reflections

There’s no doubt AI is changing how we code, learn, and approach problem-solving. But replacing a software engineer? We’re still far from that.

Some notes from this experiment:

  • I didn’t manually edit any code, even though I knew exactly what to change. That alone made a huge difference.
  • The project was based on TS, SASS, and HTML, a well-documented and widely used stack, and it still hallucinated a lot. Imagine this with Elixir, Clojure, or Crystal, I’ve tried it. It doesn’t work well.
  • Code quality was poor. It "worked", but required major rework.
  • Vague prompts result in vague answers, obvious in theory, but problematic for non-technical users.
  • The total cost was around $14, mainly from long conversations and resets. If I hadn’t started over several times, it would’ve been around $8 or 9.

Final Thoughts

After all this, I can confidently say: we’re far from singularity. LLMs are, at their core, text input → text output. Sure, there’s complex stuff in the middle, but no actual "intelligence" is happening.

If you repeat the same prompt, results may vary. That’s because LLMs are non-deterministic, they generate responses based on probabilities. Techniques like top-k sampling and top-p sampling can influence the outcome, but randomness is part of the deal.

Yes, this raises the bar for simpler tasks, and that might affect junior devs, but everything depends on context.

Now imagine a real-world complex system with backend logic, BFF, Terraform, CI/CD pipelines, or microservices, where a small UI tweak could have unexpected side effects elsewhere. That’s a mess waiting to happen. I spoke with some friends in mobile development, and their experience was even worse. Honestly, web development might be the "least bad" scenario.

It’s up to us to adapt. I use AI tools every day now, for studying, writing docs, building PRDs, MRDs, diagrams, etc. But we’re still far from AI solving software engineering on its own.

If you enjoyed this reflection, leave a comment and share your own experience using LLMs.

Until next time!

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.