Skip to content
Engineering•

August 28, 2026• 13 min read

Parens and Grace

On burnout, faith, and finding my way back to building — with a little help from Clojure.

A few months ago I set out to build a small B2B Slack app. On the surface, it’s a pretty standard team culture tool—peer appreciation, lightweight pulse checks, and small perks. But this post isn’t really about the product. It’s about the tools, the burnout that came before it, and why building it mattered to me at all.

Where I started

As a kid, I built HTML sites with Dreamweaver and hosted game servers for WoW and Counter-Strike. It was an amazing time—no work, no burden, pure freedom and creativity. Then high school happened, and I stepped away from it entirely. By university, I returned to programming with serious intent.

I started with C++, Java, and C# in university. I was eager to learn back then, trying to become a great programmer. I was in what I now call the “delusional tryhard” phase — devouring everything I could find, books, podcasts, videos, blog posts, essays. I didn’t care if a library was pre-alpha and barely had a README. If someone posted a slick benchmark on Reddit, I was using it.

Since I cared a lot about performance, I spent a lot of time on high-performance computing, computer graphics, and parallel programming — MPI, CUDA, OpenGL that whole world.

When I started my first job, I had to move to C# and .NET, but unfortunately I never managed to write anything useful. My actual job was debugging multi-million-line monolith (in the middle of microservices migration) and mentally stepping through an infamous 5k-line stored procedure in my head. Once I figured out the bug, I had to track down the specific people who had permission to touch that part of the codebase and write them an email explaining what was broken and how to fix it. I wasn’t even allowed to fix it myself.

The gap between what I was capable of learning on my own time and the bureaucratic absurdity of what the job required wore me down fast. Working on side projects was the only way to keep my skills up to date, but living that double life emptied the tank pretty fast.

After seeing that there was no future for me there, I had to leave the company.

Why this mattered

After I quit my job, I spent 8 years dealing with burnout and depression, and a few other things that haven’t made life easy.

During that time, I had to rethink a lot of things in my life. I was visiting a psychiatrist and doing everything I could to get myself back on solid ground. In the midst of all this, I returned to God, and life slowly became bearable again. Looking back, I can see those trials as being for the better.

Somewhere in that shift, I wanted to build something again—not just to ship a product, but to find some joy in making something.

The Choices

Once I left my job and had time to properly evaluate languages and ecosystems, and instead of reaching for the familiar, I went further down the functional programming rabbit hole than I expected. For a while, I thought Haskell and OCaml were the final bosses of functional programming. I sat through videos on monad transformers, worked my way up to whatever the newest hot thing was at the time, Polysemy, and was losing track of the explanation every five minutes. At some point you have to admit: Haskell isn’t for business use, at least not for someone building a Slack app.

Then I found out about Coq, Idris, and Agda and I had to stop and touch some grass.

There’s always a stronger type system one level up, with its own theory, its own proof obligations, and its own learning curve. It reframed the question for me—not “which language is best?” but “how much ceremony and proof does this problem actually need?” That’s what cleared the way for Clojure. Amen.

I also seriously considered Go. It’s simple, fast, and gets out of your way in a lot of the ways I wanted. But it felt crude in a specific way—like a machete without a handle. Effective, sure, but rough in the hand. It gets the job done, but I’d reach for it more out of necessity than enjoyment.

I’ve also never been fully comfortable in OOP languages, mainly because of the ceremony. You end up building deep class hierarchies and boilerplate just to do three basic things: fetch data, transform it, and store it.

Finding Clojure

Clojure got out of the way. I wasn’t fighting an object model or arguing with myself about the “right” abstraction. I had data—vectors and maps—and I moved it around, shaped it, stored it. That’s it.

This mattered especially with Slack’s API, an event-heavy stream of deeply nested JSON. In Clojure, you don’t need twenty DTO classes, you just reach into the map with get-in, transform it with assoc-in or update, and pass it along.

That sounds small. It wasn’t. A tool that didn’t add friction on top of everything else in my head made it possible to actually finish something.

Building against a live, running system, instead of restarting after every change, reframed what “developing” even means to me. A lot of Clojure programmers say that building against a live system feels like a conversation—and they’re completely right.

I’m not crazy

I started watching Rich Hickey’s talks—“Simple Made Easy,” “The Value of Values,” and the rest. It wasn’t the technical arguments that struck me. It was the relief of finally having words for something I’d felt for years — simplicity and ease aren’t the same thing, and most of the complexity we fight as programmers, we built ourselves.

It convinced me I wasn’t crazy—OOP really was that exhausting. After watching all his talks, I truly believe Rich Hickey is here to teach us the truth and deliver us from the Clean Code Cabal.

The community

I’m grateful for the Clojure community and the Clojurians Slack. It’s a peaceful space where people take the time to help you, no matter how basic the question. This is the first developer community I’ve ever really been part of — years of working alone, mostly heads-down, never really reaching out. Knowing a patient, supportive group is there when you’re stuck at 2 AM is a blessing.

Why Clojure?

The REPL: It’s one of the main things I like about working in Clojure. Working with a live system is an intuitive, joyful experience. It’s the thing that got me into a flow state much more easily, and kept me there through long sessions. I honestly didn’t know, going in, that interacting with a live system would end up being this well-suited to my mind. It wasn’t something I was looking for, it just turned out to fit. There’s something genuinely peaceful about it too — no waiting, no losing your place, just staying in the conversation with the code.

Small Syntax & Immutability by Default: Clojure’s syntax is tiny: a handful of special forms, a few core data structures, and everything else is just functions. Having so little syntax to keep in my head was a massive relief.

And immutability gave me total confidence: functions take data and return new data, without secretly mutating things behind the scenes. Immutability by default should be the baseline for modern programming.

Multimethods for Events & Notifications: Slack sends you a lot of event types — messages, reactions, app mentions, interactive payloads, slash commands, and modal submissions. In most stacks, this turns into a giant if/else or switch statement, or a pile of handler classes that all implement the same interface.

In Clojure, multimethods let you dispatch on arbitrary data, like a composite vector of [event-type callback-id]:

(defmulti handle-slack-event
  (fn [payload]
    [(:type payload) (get-in payload [:view :callback_id])]))

(defmethod handle-slack-event ["view_submission" "bot_channel_setup"]
  [{:keys [user view] :as payload}]
  (let [channel-id (get-in view [:state :values :channel_select :action :selected_channel])]
    (configure-workspace-channel! (:id user) channel-id)
    {:response_action "clear"}))

(defmethod handle-slack-event :default [payload]
  (log/warn "Unhandled Slack event:" (:type payload)))

I used the exact same pattern on the outgoing side in the background worker. When an event happens (like approving a reward), the backend drops a simple message onto a Redis queue:

(notification/notify! redis :perk/fulfilled user {:item-name item-name :workspace-id workspace-id})

The notification worker receives the message and runs it through format-slack-notification to assemble the payload:

(defmulti format-slack-notification (fn [message] (:type message)))

(defmethod format-slack-notification :perk/fulfilled
  [{:keys [user data]}]
  {:destination-type :dm
   :slack-id         (:slack-user-id user)
   :text             (format "🛍️ Good vibes incoming. ✨ Your *%s* is all set." (:item-name data))})

Then a generic delivery function reads the returned data, fetches the workspace bot token from Vault, and sends the message:

(defn- process-notification! [message]
  (let [workspace-id (get-in message [:data :workspace-id])
        bot-token    (:bot-token (vault/read-cached-secret (str workspace-id "-bot-token")))
        notification  (format-slack-notification message)]
    (when notification
      (let [{:keys [destination-type slack-id channel-id text blocks]} notification]
        (case destination-type
          :dm        (slack/send-message! (slack/client) bot-token slack-id text :blocks blocks)
          :channel   (slack/send-message! (slack/client) bot-token slack-id text :blocks blocks)
          :ephemeral (slack/send-ephemeral-message! (slack/client) bot-token channel-id slack-id text :blocks blocks))))))

Adding a new notification or interactive event never requires touching existing conditionals or restructuring classes — you just add a new defmethod. It’s very elegant.

SQL as Pure Data (No ORM Drama): I used HoneySQL. SQL queries are just plain Clojure maps. Even multi-table JOINs with pagination are just clean data structures:

(defn recognitions [datasource limit offset]
  (let [query {:select [:pr.*
                        [:giver.first_name :giver_first_name]
                        [:receiver.first_name :receiver_first_name]]
               :from   [[:recognitions :pr]]
               :join   [[:users :giver] [:= :pr.giver_user_id :giver.user_id]
                        [:users :receiver] [:= :pr.receiver_user_id :receiver.user_id]]
               :order-by [[:pr.created_at :desc]]
               :limit  limit
               :offset offset}]
    (jdbc/execute! datasource (sql/format query))))

Integrant & Aero: The entire system — database pools, Redis connections, HTTP server, secrets — is defined declaratively in a single config.edn file using Aero for environment resolution and Integrant for dependency wiring:

{:app/server {:port    #profile {:dev 4000 :prod #env APP_SERVER_PORT}
              :handler #ig/ref :app/api}

 :app/api    {:database #ig/ref :app/database
              :redis    #ig/ref :app/redis
              :vault    #ig/ref :app/vault
              :slack    {:client-id      #env SLACK_CLIENT_ID
                         :signing-secret #env SLACK_SIGNING_SECRET}}

 :app/database {:config #profile
                         {:dev  {:dbtype "postgresql" :dbname "mellowe"}
                          :prod {:dbtype "postgresql" :host #env POSTGRES_HOST}}}}

Then in code, Integrant uses simple multimethods to initialize and tear down each component in topological order:

(defmethod ig/init-key :app/server [_ {:keys [handler port]}]
  (log/info (str "Starting jetty server on http://localhost:" port))
  (run-jetty handler {:port port :join? false}))

(defmethod ig/halt-key! :app/server [_ server]
  (log/info "Stopping jetty server")
  (.stop server))

Integrant reads the #ig/ref tags and handles starting and stopping dependencies cleanly. At the REPL, reloading the entire system state takes about a second.

Polylith: I structured the codebase using Polylith. Business logic lives in decoupled, reusable components (like recognition, workspace, and market). Both the API backend and the background notification worker reuse the exact same core components directly. It gave me the modularity of microservices with the simplicity of a single monorepo.

Polylith Workspace Info

Full-Stack with ClojureScript & re-frame: For the web app I chose ClojureScript with Reagent and re-frame.

UIx is more popular now and more React-idiomatic, but I preferred Hiccup syntax. Reagent is a bit slower than UIx’s more direct React interop, but for a small app like Mellowe, that difference never mattered.

What I didn’t like was the JS interop part of this stack. If it weren’t for AI handling most of that translation layer for me, I would’ve burned a lot of hours just getting the analytics charts wired up and rendering correctly. There was also a lot of Keycloak setup for auth which AI helped with.

Stability: If I had to name the single most important thing, it’s the language’s stability. I don’t like things moving and changing constantly under my feet—I just don’t have the energy for the churn anymore. Clojure evolves deliberately and treats backwards compatibility with real respect.

That mattered to me more given where I started—that “delusional tryhard” phase I mentioned earlier. I’ve gotten more honest about what I actually need from a tool, and stability turned out to be near the top of that list.

Making peace with dynamic typing

I’d be lying if I said giving up a type system cost me nothing, at least in theory. I started with statically typed languages and only dabbled in JavaScript. Every project I’d built prior was in C++, C#, or Java. My brain was used to static typing and OOP, and I expected to pay for leaving it.

In my experience working with Clojure, though, the runtime errors barely happened. Building incrementally in the REPL, checking the shape of data at each step, meant I caught those issues before they became bugs. LSP with static types might give you faster diagnostics, but developing against a live system is the way I enjoy working.

There’s also clj-kondo, which helps a lot too — flagging errors statically before you even run anything.

For my ~44k-line project, the REPL, immutability by default, and schemas are more than enough.

What was painful (at first)

Coming in with zero Lisp background, the parens were rough. My eyes just refused to parse the nesting, counting brackets, losing track of which form I was even inside. That was not fun.

It took me a few weeks to get comfortable and for the parens to “disappear.”

Using [] for vectors and {} for maps alongside () for calls also helped a lot.

Funny enough, now that I’ve fully acclimated to Clojure, going back to reading C-style syntax with all the braces and semicolons feels somewhat painful. I guess you just get used to whatever you spend the most time in.

T-1000 is coming

I use Antigravity with Gemini, and most of the time it generates proper, working code—except when it goes absolutely off the rails and I’m left staring at the output wondering what just happened. Sometimes I get angry and start typing bad words at our future AI overlords, and after a while, I get timed out. Coincidence? I don’t think so.

My workflow is simple: If a feature is large, I split it into separate tasks instead of throwing the whole thing at it at once. Once the agent finishes, I review and evaluate the code myself to check it actually works. I don’t commit code I can’t review or understand. Maybe I’m just not AI-savvy enough, but that’s my workflow at the moment.

If I were doing this in TypeScript or C#, I know what would happen: the AI would generate 150 lines of boilerplate, my eyes would glaze over, and I’d end up just skimming through it, hoping it works, and shipping it. Clojure is the reason I can actually audit my codebase. Because it’s just data transformations and pure functions, I can look at a 10-line diff and understand what my system is doing.

I started writing Mellowe completely without AI. Now, much of my day is spent prompting, evaluating, and fixing. I still love testing things live in the REPL rather than restarting a server, but the enjoyment of the effort is gone. I feel more productive, but also more tired.

When I was writing the code myself, line by line, there was genuine joy and appreciation in the mix. I was tired then too, but it was a “good tired”. Now, the flow state is almost gone, and my attention span is shorter. Instead of living inside the code, I’m spending all day reviewing and auditing it. I’ve always been more of a results person, but even so, the process of building something just isn’t the same anymore.

I tried to make sense of what AI is doing to the way we build software, but at this point, I just can’t afford to dwell on it. I’ve decided to use it deliberately, without going to extremes.

Where I’m going

Taking Mellowe from an empty repo to a shipped product gave me back something I thought I’d lost: the confidence to build again.

The tools and the industry will keep shifting, but Clojure gave me a quiet, sane foundation when I needed it most. Going forward, I’ll keep using it wherever I can.

As for everything else, I continue through life trusting God. Finding peace and getting my life back was pure grace.

Thanks for reading, and God bless you!