Assorted Clojure web app notes
2025-11-30
I've been working almost exclusively in Clojure over the past five or six months as a learning exercise. This is a collection of my notes on what I've learned so far. Most of these notes are validations of what Rich Hickey, Clojure's creator, mentions in his talks.
General notes
- Different types of changes have different blast radii. Changes to the application concept, due to misconceptions about the domain or changes in fundamental assumptions, have the biggest and worst blast radius and very often require changes to everything that follows. Changes to the application database's schema, which should be a reflection of the conception the software developers have of the domain, have a bad blast radius that is significantly more manageable than the blast radius of a misconception.
- Codebase problems are solved most effectively if the amount of code relevant to the problem can fit in your personal working memory. There is a notable step-function decrease in your effectiveness as a programmer if the problem you are working on is even slightly too large for your working memory.
- Keeping logic in pure transformations of data is a particularly effective way of containing the blast radius of any type of change, as purity reduces the number of dimensions in which you need to work and thus increases the effectiveness of your personal working memory.
- Programmers use the term "abstraction" often to mean the extraction of logic-performing code into distinct functions. I believe this concept is better served by the term "factorization."
- Factorization only helps if the extracted logic is truly separate from the whole. A particularly reliable way to achieve this characteristic is to factorize pure data transformations.
- False factorizations greatly increase the cost of loading code into your personal working memory.
- Purity enhances testability, not only in automated testing but in REPL testing. Purity also enhances observability in all senses.
- In my experience, using the options map for arguments that are not clearly and obviously core to the function is a particularly effective way of decomplecting changes to the function from its callsites. Interestingly, this may also be a negative characteristic if you want to strictly require changes to callsites upon changing a function.
- Greppability is an underrated characteristic of a codebase. This is why I now retrieve my configuration data with a dedicated
config-valfunction instead of using the configuration map atom directly, as there are many ways then of getting a value (e.g.,get-in, threading). - Git commits are effective if treated as the thread of Ariadne. I have a greatly increased respect for the practices of writing meaningful commit messages and of adding tags.
- Please read the Git book. It is short and comprehensible and will pay dividends forever.
- It seems to be best to keep feature branches from growing too large. Merge them back to main at sensible, short intervals. If you can't easily see a conceptual gap between main and HEAD and instead think of HEAD as its own codebase, you're too far from main.
- Re-normalization of SQL rows, initially de-normalized by joins, into nested maps for the purposes of transmission to the frontend is an interestingly common scenario.
- The power of raw SQL is still unmatched. I tried HoneySQL for a while. While I can see value in HoneySQL, especially around constructing dynamic queries, I found myself turning to raw SQL for most of the non-trivial queries I needed to do.
- I finally learned enough shortcuts on VS Code to be able to use only the keyboard for 90% of my common tasks. It turns out that this wasn't hard. This has significantly reduced my desire to switch to another editor.
How to change software
- When you're conceptualizing a new feature, get yourself a sandbox and play with the various concepts involved. If external software is involved, try to get the intent behind the feature/bugfix to work with the native tools. This step is mandatory. Gather as much data as you can about the behavior of the space you'll be walking into.
- When revisiting an old project, or even a project that you just haven't touched for a few days, resist the urge to preemptively refactor the architecture to fit your latest tastes. Such refactors must earn their right to be implemented.
- When dealing with a misconception (e.g., moving SimGen from a chat interface to a vibecoding interface), it helps to just leave the old database tables behind and implement the new version of the app from "scratch" with new tables. Avoid modifying/deleting old data and schemas if possible.
Things I thought were good ideas and were
- I made a decision early on to have a
configmap atom that was loaded in from thejava -jarcommand as a CLI arg for an EDN file. I have never questioned this decision except when I had to use environment variables for compliance. - I realized early on that I was basically making my own web framework. I keep and update a Git repo of that web framework, which I call
cljwebapp. This has been one of the greatest software investments I've ever made. - To get config vals, I use a
config-valfunc. This is just a wrapper around(get-in @config (vec args)), but this makes sites that use config values very easily greppable.
Things I thought were good ideas but weren't
- In the beginning, I forced all database queries to go through a function in either the
queries.cljorcommands.cljfiles. I then added a directdb/exec!function that would accept either next.jdbc query vecs or HoneySQL maps. This introduced more than one way to do things, which meant there was more than one thing to grep for.
Justified tech debt
- Several of my projects (Litechat, SimGen) had a need to manage files. I originally stored some of these files in the database, since I anticipated that they would only be text files. This eventually came back to haunt me as the use cases evolved to require binary files, so I had to migrate file storage to URI-based storage. However, keeping the initial use cases narrow let me deploy faster, so I think this was justified, if annoying.