A learning project for exploring Clojure web development patterns, featuring a full-stack web application with PostgreSQL, background jobs, and REPL-driven development.
This project is based on the amazing tutorial https://caveman.mccue.dev. It is a "caveman-themed" web application built to learn and demonstrate idiomatic Clojure patterns for web development. It showcases:
- System lifecycle management with the "system map" pattern
- REPL-driven development workflow
- Database-backed background jobs using triggers
- Server-side HTML rendering with Hiccup
- Testing with real databases using Testcontainers
- Modern Clojure tooling (Portal, next.jdbc, Reitit, etc.)
- Language: Clojure 1.12.0
- Web Server: Ring + Jetty
- Routing: Reitit (data-driven routing)
- HTML: Hiccup (Clojure → HTML)
- Database: PostgreSQL 17
- Connection Pool: HikariCP
- Database Access: next.jdbc
- SQL Generation: HoneySQL
- Background Jobs: Proletarian (Postgres-backed queue)
- Migrations: MyBatis Migrations
- Testing: Kaocha + Testcontainers
- Dev Tools: Portal (data visualization), nREPL
caveman-project/
├── src/caveman/ # Application source code
│ ├── main.clj # Entry point for production
│ ├── system.clj # System lifecycle management
│ ├── routes.clj # Root router
│ ├── middlewares.clj # Ring middleware stacks
│ ├── jobs.clj # Background job dispatcher
│ ├── page_html/ # HTML layout components
│ ├── cave/ # Cave feature (routes + jobs)
│ ├── hello/ # Hello world route
│ ├── goodbye/ # Goodbye route
│ └── static/ # Static file serving
├── dev/ # Development-only code
│ ├── user.clj # REPL helpers (start/stop system)
│ └── portal.clj # Portal debugging setup
├── test/caveman/ # Tests
│ └── test_system.clj # Testcontainers test infrastructure
├── migrations/ # Database migrations
│ ├── scripts/ # SQL migration files
│ └── environments/ # Migration configuration
├── res/ # Resources (static files)
├── deps.edn # Dependencies and aliases
├── Justfile # Task runner (like Make)
└── docker-compose.yaml # Postgres container for development
The application uses a "system map" to manage stateful components:
{::system/env ; Environment configuration
::system/cookie-store ; Session store
::system/db ; Database connection pool
::system/worker ; Background job worker
::system/server} ; HTTP serverComponents are started in dependency order and stopped in reverse order. This pattern enables:
- Clean startup/shutdown
- REPL-driven development (restart components without restarting JVM)
- Dependency injection (pass system map to handlers)
A unique pattern for decoupling operations:
- Insert a cave → Database trigger fires
- Trigger creates job → Inserts into
proletarian.jobtable - Worker polls jobs → Processes job asynchronously
- Job handler runs → Creates a hominid for the cave
Benefits:
- HTTP requests return immediately
- Database guarantees job creation (same transaction)
- Automatic retries on failure
- Can scale workers independently
See: migrations/scripts/20251003180736_cave_insert_trigger.sql
Development workflow centers around the REPL:
;; In dev/user.clj
(start-system!) ; Start everything
(db) ; Get database connection
(jdbc/execute! (db) ...) ; Run queries
(restart-system!) ; Reload changes
(stop-system!) ; Clean shutdownThe system supports hot reloading in development mode - just re-evaluate changed functions!
Portal provides visual inspection of data:
(tap> {:my "data"}) ; Send data to Portal UIPortal opens in your browser and shows beautiful, interactive views of your data.
Tests use Testcontainers to run real PostgreSQL:
(test-system/with-test-db
(fn [db]
;; db is a fresh, migrated database
(jdbc/execute! db ["INSERT INTO ..."])
(is (= ...))))Each test gets an isolated database cloned from a template (fast!).
- Java 23+ (check:
java -version) - Clojure CLI (install: https://clojure.org/guides/install_clojure)
- Docker (for Postgres and tests)
- Just (optional task runner, install:
brew install just)
-
Clone the repository
git clone git@github.com:RegiByte/clojure-caveman.git caveman-project cd caveman-project -
Create
.envfile in project root:PORT=8080 ENVIRONMENT=development POSTGRES_USERNAME=postgres POSTGRES_PASSWORD=yourpassword
-
Start PostgreSQL
docker-compose up -d
-
Run migrations
# Manual migration with MyBatis Migrations migrate up
-
Start nREPL server
just nrepl # or: clojure -M:dev -m nrepl.cmdline -
Connect your editor (Calva, Cursive, CIDER, Emacs, etc.)
-
Start the system
(start-system!) -
Visit http://localhost:8080
-
Make changes and re-evaluate functions - no restart needed!
just run
# or: clojure -M -m caveman.mainVisit http://localhost:8080
just help # List all commands
just run # Run the application
just nrepl # Start nREPL server
just test # Run tests
just format # Format code
just format_check # Check formatting
just lint # Run clj-kondo linter
just outdated # Check for outdated dependencies-
Create migration
migrate new "migration_name" -
Edit the generated SQL file in
migrations/scripts/ -
Run migration
migrate up
| Method | Path | Description |
|---|---|---|
| GET | / | Hello world with DB query |
| GET | /goodbye | Goodbye message |
| GET | /cave | List caves + creation form |
| POST | /cave/create | Create new cave |
| GET | /favicon.ico | Favicon |
| GET | /nested/file | Static file example |
id(uuid, primary key)created_at(timestamptz)updated_at(timestamptz, auto-updated by trigger)description(text)
Trigger: On insert → creates background job
id(uuid, primary key)created_at(timestamptz)updated_at(timestamptz)name(text)cave_id(uuid, foreign key to cave)
Created by: Background job when cave is inserted
- (Created by Proletarian migrations)
- Stores background jobs with retry logic
Run all tests:
just test
# or: clojure -M:dev -m kaocha.runnerTests automatically:
- Start PostgreSQL in Docker (Testcontainers)
- Run migrations against template database
- Create isolated database per test
- Clean up after each test
just lint # Run clj-kondojust format_check # Check if formatted
just format # Auto-format codejust outdated| Variable | Description | Example |
|---|---|---|
| PORT | HTTP server port | 8080 |
| ENVIRONMENT | development or production | development |
| POSTGRES_USERNAME | Database user | postgres |
| POSTGRES_PASSWORD | Database password | yourpassword |
This project uses many Clojure idioms and libraries. Here are resources for learning more:
- Clojure for the Brave and True
- Clojure Official Docs
- ClojureDocs - Community documentation
- Ring - HTTP abstraction
- Reitit - Routing
- Hiccup - HTML generation
- next.jdbc - Database access
- HoneySQL - SQL generation
- Proletarian - Background jobs
- Portal - Data visualization
- Component - System architecture (similar pattern)
- REPL-Driven Development
-
Var indirection (
#'my-functioninstead ofmy-function)- Allows hot reloading from REPL
- Use in development mode
-
Namespace aliases (
:as-aliasinnsform)- Use namespaced keywords without requiring the namespace
- Prevents circular dependencies
-
Threading macros (
->,->>,as->)- Clean up nested function calls
as->when you need to pass to different positions
-
Rich comment blocks
- Use
(comment ...)for REPL experiments - Won't execute on load but can be evaluated manually
- Use
-
tap> when debugging
- Debug by sending data to Portal
- Much better than
println
- Reflection warnings: If you see them, add type hints or
set! *warn-on-reflection* true - Keywords vs strings: Database/JSON boundaries often involve conversion
- CSRF tokens: All POST requests need
(anti-forgery-field)in the form
Ideas for extending this project:
- Add authentication/authorization
- Add API endpoints (RESTful JSON API)
- Add WebSocket support for real-time updates
- Add more comprehensive tests
- Add logging with structured logging (e.g., timbre)
- Add metrics/monitoring (e.g., Prometheus)
- Add production deployment (uberjar, Docker)
- Add ClojureScript frontend
- Add email sending via background jobs
- Add scheduled jobs (cron-like)
Educational project - do whatever you want with it!
Built as a learning project to understand Clojure web development patterns. Thanks to the Clojure community for excellent libraries and documentation! But the special thanks go to @bowbahdoe for the amazing caveman tutorial!
Note to future self: You built this to learn Clojure! The patterns here are intentionally explicit to help you remember the concepts. Don't be afraid to refactor as you learn more. Happy hacking! 🚀