Time really flies - we are already at the end of February! The first 60 days of 2025 have zoomed past us. Speaking of moving fast, I'm reminded of one of the key decisions we made at Swym that has helped us keep up and "move slow to move fast": our choice to use Clojure. Why am I even talking about this topic you wonder - I haven’t shipped anything in Clojure for ages (maybe nothing in other tech too 🤐), so the top of mind question was how usable/relevant Clojure is to our future investments. (Also that we wanted to ship a lean, scalable and iteration friendly workflow scheduling layer)
Background context
At Swym, we build software (SaaS) for our e-commerce merchants (over 42,000 and counting!) that can handle a ton of scenarios, customizations, and scale spikes (like BFCM - every year is crazier than the last, stepping up from Millions+ to Billions+ requests in quick time! 🔥). Our guiding principles when we started were:
We wanted to be a tiny team
We wanted teams of 1 to own large areas of responsibility
We wanted to spend most of our time on the problem, the technology choice following the solution
We wanted to write the least amount of code as possible
We wanted to run the systems at the least cost as possible
Although Clojure was already in use at Swym when I joined (it was part of the JD I applied to even), we still had to decide whether to stick with it or try something else.
We decided to go all-in with Clojure, and today, most of our backend runs on it. The inherent efficiency Clojure brings has allowed us to keep our engineering teams lean and mean, which continues to impress me.
Clojure's Relevance to Our Future
Coming to the “how usable/relevant Clojure is to our future” note. Despite all the benefits, this question is something we discuss for a few reasons, some below:
Clojure, for better or worse, can be seen as an introverted language and ecosystem. Its unique syntax and symbolism can be perceived as arcane, closed off, and difficult to approach. This perception can create a barrier to entry for plenty of smart developers with friendlier choices.
While Clojure has a dedicated following, it's true that the number of people deeply familiar with the language is relatively small. I’d guess that less than ~5% of those who have used Clojure would consider themselves proficient. Clojure by default is run by the community of open-source contributors and that can lead to a lot of unresolved debates on “standards”. With lower confidence on how the language works, most of the “standard” practices are typically opinionated from each’s experience and usage, thereby making it tough to define “good” Clojure code unlike other language ecosystems.
Those who do grasp Clojure tend to form a lifelong bond with it. Very few developers who have truly embraced Clojure would willingly choose to code in another language. This can create an unnecessary bias on technology choice being more important than what the problem needs - that’s a hard balance to strike.
New members of our engineering team frequently ask, "Why Clojure?" and often express concerns that it feels outdated, complex, obscure, or disconnected. I daresay some still feel that way. This requires newer folks to embrace an unknown which can cause a confidence barrier especially those who have experience building things elsewhere. (I suppose this is a universal concern, not Clojure or Swym specific)
Another controversial decision has been our heavy usage of macros for models, queues, processors, etc. Macros are a no-no for many Clojure folks because it hurts readability/debugging over time as teams grow, it needs the individual to really get into the details. On that note, we shake and agree-to-disagree, focussing back on the guiding principles - We want to stay lean and go deep, so we will make choices to support that. (Biased opinion - Macros are fun and just wow when used effectively!)
As we scaled up our team, we wanted to avoid becoming a tower of babel of technologies, we came to a reasonable agreement over time to limit the production stack choices. As of today we use a combination of Clojure, Node/Typescript, some Python and anything needed for frontend experiences (be it pure vanilla or frameworks or homegrown protocols). We continue to evaluate this decision at every new milestone, including in 2025. Ended up being my trigger to document our thinking including principles that guides our long term decisions.
Non-Zero-Sum games
All this was a long-winded way of saying that despite the challenges, the decision to keep and foster Clojure has been a net positive - yes, it even enabled an out-of-touch contributor (i.e. me) to ship that generic scheduling API in a couple of hours with ~150 new lines of code and little to no AI magic involved (yikes 😱).
* Of course not always, always until conditions remain true 😉
Anyways, getting back to what I actually did - Here is a brief snippet for reference with all the macro mania abstracted away eg: `defqueue`, `defprocessor`, `tracing/with-span`, etc do a lot of heavy lifting for us.
(def WorkflowParams
{:pid s/Str
:workflow-id s/Str
:workflow-type s/Str
:workflow-params s/Any
:recurring s/Bool
:schedule-time FutureDate
(s/optional-key :crontab) (s/maybe s/Str)
:exit-listener s/Any
:cancelled s/Bool})
(defqueue ScheduleWorkflow
:qid "q:schedule-workflow-req"
:schema WorkflowParams
:versioning true)
(defn handler-request
[{:keys [pid workflow-id workflow-type workflow-params recurring schedule-time crontab cancelled] :as message}]
(tracing/with-span
["handler-workflow-request"
{:pid pid :workflow-id workflow-id
:workflow-type workflow-type
:workflow-params workflow-params :recurring recurring
:schedule-time schedule-time :crontab crontab}]
(try
(let [{:keys[headers status body] :as response} (http/post (:url workflow-params) {:headers (:headers workflow-params) :body (:body workflow-params)})]
(log/info "handler-workflow-request response" {:headers headers :status status :body body}))
(catch Exception e
(log/error "Error handler-workflow-request " (str e) workflow-id e)))))
(defn message-handler
[{:keys [pid workflow-id workflow-type workflow-params recurring schedule-time crontab cancelled] :as message}]
(let [app WISHLIST
message (assoc message :app app)]
(tracing/with-span
[(str "dequeue schedule-workflow")
{:pid pid
:workflow-id workflow-id :workflow-type workflow-type
:workflow-params workflow-params :recurring recurring
:schedule-time schedule-time :crontab crontab
:cancelled cancelled}]
(if cancelled
(tracing/with-span
["cancelled true"
{:pid pid :workflow-id workflow-id}]
(log/info "Skipping workflow as it is cancelled" message))
(tracing/with-span
["cancelled false"
{:pid pid :workflow-id workflow-id}]
(handler-request message))))))
(def ^:const max-retry 5)
(defn retry-check-fn
[{:keys [params] :as msg} attempt]
(<= attempt max-retry))
(defn notify-after-last-retry [{:keys [pid] :as msg} attempt err]
;;raise hell aka send webhook/ping
(raise-hell-somewhere-someone msg attempt err))
(defprocessor ScheduleWorkflowProcessor
'schedule-workflow-namespace
message-handler
{:retry?-fn retry-check-fn :fail-fn notify-after-last-retry})
(defroutes workflow-routes
(context "/workflows" []
:tags ["Workflows"]
:query-params [pid :- s/Str]
(POST "/" []
:body [params WorkflowParams]
:summary "Create a new workflow"
(ScheduleWorkflow/create params))
(PUT "/:workflow-id" []
:path-params [workflow-id :- s/Str]
:body [params WorkflowParams]
:summary "Update an existing workflow"
(ScheduleWorkflow/update (assoc params :workflow-id workflow-id)))
(POST "/:workflow-id/reschedule" []
:path-params [workflow-id :- s/Str]
:body-params [schedule-time :- FutureDate]
:summary "Get a workflow"
(ScheduleWorkflow/reschedule pid workflow-id schedule-time))
(POST "/:workflow-id/cancel" []
:path-params [workflow-id :- s/Str]
:body-params [schedule-time :- FutureDate]
:summary "Cancel a workflow"
(ScheduleWorkflow/delete pid workflow-id))
(DELETE "/:workflow-id" []
:path-params [workflow-id :- s/Str]
:summary "Delete a workflow"
(ScheduleWorkflow/delete pid workflow-id))
(GET "/:workflow-id" []
:path-params [workflow-id :- s/Str]
:summary "Get a workflow"
(ScheduleWorkflow/get pid workflow-id))))
Adding AI support to IDEs might have sped up my work, but the underlying simplicity (and potential pitfalls 🥲) is inherently tied to Clojure and our development choices. Playing non-zero-sum games is hard especially long-term bets - As always, we continue to learn and adapt. 🙌
Obvious note - I am 1000% sure there are other languages and stacks that have elegance and depth (and probably even better). So much more to uncover here as our access to opportunities continues to grow. Not to mention all the cool vibe coding tools (aka AI-driven IDEs) making things far simpler, never been a better time to “repl”!!
Thats an interesting and defining question to grapple with Aravind!!