# Deployment and DevOps Standards for Clojure
This document outlines the standards for deploying and operating Clojure applications. It provides guidance and best practices to maintain code quality, ensure reliable deployments, and optimize operational efficiency.
## 1. Build Processes and CI/CD
A robust CI/CD pipeline is essential for delivering Clojure applications reliably. It automates building, testing, and deploying your code, reducing manual effort and minimizing errors.
### 1.1. Dependency Management
Clojure projects typically use Leiningen, Boot, or more recently, tools.deps for dependency management. tools.deps is now the recommended approach.
**Do This:** Use "deps.edn" for dependency management and "clj" command-line tool.
**Don't Do This:** Rely on outdated tools or manual dependency management. Older project.clj files are strongly discoruaged for new projects.
**Why:** "deps.edn" is the official dependency management solution for Clojure, providing a more streamlined and reproducible build process. It's also inherently more declarative.
**Example:**
"""clojure
;; deps.edn
{:deps {org.clojure/clojure {:mvn/version "1.11.2"}
ring/ring-core {:mvn/version "1.9.6"}
ring/ring-jetty-adapter {:mvn/version "1.9.6"}
com.cognitect.transit/transit-clj {:mvn/version "0.8.339"}
org.slf4j/slf4j-simple {:mvn/version "2.0.9"}
}
:aliases
{
:test
{:extra-paths ["test"]
:dependencies [[org.clojure/test.check {:mvn/version "1.1.1"}]]
:main-opts ["-m" "clojure.test runner"]}
:repl
{:extra-paths ["dev"]
:dependencies [[integrant/integrant {:mvn/version "0.8.0"}]
[integrant/repl {:mvn/version "0.3.0"}]]
:main-opts ["-m" "user"]}
:eastwood
{:extra-paths ["dev"]
:dependencies [[jonase/eastwood {:mvn/version "1.4.0"}]]
:main-opts ["-m" "eastwood.lint" "[src test dev]"]}
}
:paths ["src"]}
"""
### 1.2. Build Automation
Automated builds ensure consistent and reproducible results.
**Do This:** Use "clj" combined with shell scripts or a build tool like "bb" (Babashka) to automate the build process. Babashka builds are significantly faster.
**Don't Do This:** Manually compile and build your Clojure applications.
**Why:** Automation reduces errors, promotes consistency, and allows for repeatable deployments.
**Example using "bb":**
"""clojure
;; build.clj
(require '[babashka.process :as p]
'[clojure.string :as str])
(defn uberjar []
(println "Creating uberjar...")
(let [{:keys [exit]} (p/process ["clj" "-A:uberjar" "-m" "clojure.main" "-e" "(require 'my-app.core) (my-app.core/-main)"])]
(if (not= exit 0)
(throw (ex-info "Uberjar creation failed" {:exit exit}))
(println "Uberjar creation complete."))))
(defn deploy []
(println "Deploying...")
(let [{:keys [exit]} (p/process ["scp" "target/my-app.jar" "user@server:/path/to/deploy/"])]
(if (not= exit 0)
(throw (ex-info "Deployment failed" {:exit exit}))
(println "Deployment successful."))))
(comment
(uberjar)
(deploy))
"""
**To run the build from the command line:** "bb build.clj"
This uses Babashka to shell out to "clj" with an alias that builds the Uberjar and then deploys it via "scp".
**Example "clj" alias "uberjar" in "deps.edn"**
"""clojure
:uberjar
{:extra-deps {io.github.clojure/tools.build {:git/tag "v0.9.9" :git/sha "e013506"}}
:ns-default :uberjar
:exec-fn shadow.cljs.devtools.server/start
:main-opts ["-m" "my-app.uberjar/create-uberjar"]
}
"""
**Why use Babashka "bb"?**
Babashka scripts start in milliseconds, compared to the seconds it takes a JVM Clojure to start. For simple build or utility scripts, "bb" is ideal.
### 1.3. Continuous Integration
CI systems automate testing and building code on every commit or pull request.
**Do This:** Integrate your Clojure project with a CI platform like GitHub Actions, CircleCI, Jenkins, or GitLab CI.
**Don't Do This:** Skip automated testing or rely on manual testing alone.
**Why:** CI provides rapid feedback on code changes, preventing errors from reaching production.
**Example using GitHub Actions:**
"""yaml
# .github/workflows/main.yml
name: CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Install Clojure CLI
run: |
curl -O https://download.clojure.org/install/linux-install.sh
chmod +x linux-install.sh
sudo ./linux-install.sh
- name: Run tests
run: clj -A:test
"""
This workflow checks out the code, sets up Java, installs the Clojure CLI, and runs tests defined in the ":test" alias in "deps.edn".
### 1.4. Continuous Deployment
CD automates the deployment of your application after successful CI builds.
**Do This:** Integrate your CI system with a deployment platform like AWS, Google Cloud, Azure, or a container orchestration system like Kubernetes.
**Don't Do This:** Manually deploy your application.
**Why:** CD streamlines the deployment process, enabling faster releases and reducing the risk of human error.
**Considerations:**
* **Blue/Green Deployments:** Minimize downtime during deployments.
* **Canary Releases:** Gradually roll out changes to a subset of users to monitor for issues.
* **Feature Flags:** Enable or disable features without deploying new code.
### 1.5 Containerization
Using containers provides reproducible and isolated environments
**Do this:** Create a "Dockerfile" to package your application.
**Don't do this:** Deploy directly to a host without containerization, except in very specific circumstances
**Why:** Containers provide a standardized environment for your application, reducing discrepancies between development, testing, and production environments.
**Example "Dockerfile":**
"""dockerfile
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY deps.edn ./
COPY src ./src
COPY resources ./resources
RUN clojure -A:uberjar
EXPOSE 3000
CMD ["java", "-jar", "target/my-app.jar"]
"""
This "Dockerfile" uses a base Java image, copies the source code and dependencies, builds an uberjar, and then runs the application when the container starts
## 2. Production Considerations
Deploying Clojure applications to production requires careful planning and configuration.
### 2.1. AOT Compilation
Ahead-of-Time (AOT) compilation improves startup time and runtime performance.
**Do This:** AOT compile critical namespaces in your application.
**Don't Do This:** Run your application without AOT compiling performance-sensitive code if you have long startup times or performance issues.
**Why:** AOT compilation reduces startup latency and improves runtime performance by pre-compiling Clojure code to Java bytecode. Use clj-kondo to statically analyse your code before AOT compilation.
**Example:**
In "deps.edn" within the ":uberjar" alias:
"""clojure
:aot :all
"""
This will AOT compile all namespaces within the project. Alternately namespaces can be listed explicitly.
### 2.2. JVM Tuning
Properly tuning the Java Virtual Machine (JVM) optimizes resource utilization and application performance.
**Do This:** Consider using a modern garbage collector like G1 or ZGC. Monitor JVM resource usage (memory, CPU). Set appropriate heap size based on application requirements.
**Don't Do This:** Use default JVM settings without considering your application's needs. Use the SerialGC garbage collector.
**Why:** JVM tuning can significantly improve performance, reduce memory consumption, and optimize garbage collection.
**Example:**
Pass JVM options when starting the application:
"""bash
java -Xms2g -Xmx2g -XX:+UseG1GC -jar target/my-app.jar
"""
This sets the initial and maximum heap size to 2GB and enables the G1 garbage collector.
### 2.3. Logging
Comprehensive logging is crucial for debugging and monitoring production applications.
**Do This:** Use a logging library like "clojure.tools.logging" or "timbre" to log events at appropriate levels (debug, info, warn, error). Log structured data using JSON or EDN as this drastically simplifies querying.
**Don't Do This:** Rely on "println" for logging or omit important information from log messages.
**Why:** Structured logging facilitates searching, filtering, and analyzing log data stored in centralized logging systems like Elasticsearch or Splunk.
**Timbre Example:**
"""clojure
(ns my-app.core
(:require [taoensso.timbre :as log]))
(defn process-data [data]
(log/info "Processing data:" (log/spy :data data))
(try
;; ... process data here ...
(log/debug "Data processed successfully")
:success
(catch Exception e
(log/error e "Error processing data")
:failure)))
"""
### 2.4. Monitoring and Alerting
Monitor your application's health and performance and set up alerts for critical issues.
**Do This:** Use monitoring tools like Prometheus, Grafana, Datadog, or New Relic to track key metrics (CPU usage, memory consumption, response times, error rates). Set up alerts to notify you of critical issues.
**Don't Do This:** Ignore application health or rely on manual monitoring.
**Why:** Proactive monitoring and alerting allow you to identify and resolve issues before they impact users.
**Example using Prometheus and Grafana:**
1. **Expose metrics:** Use a library like "metrics-clojure" to expose application metrics in a format Prometheus can scrape.
2. **Configure Prometheus:** Configure Prometheus to scrape the exposed metrics endpoint.
3. **Visualize metrics:** Use Grafana to create dashboards visualizing the collected metrics.
4. **Set alerts:** Configure Prometheus Alertmanager to send notifications when metric thresholds are exceeded.
### 2.5. Configuration Management
Externalize application configuration to easily adapt to different environments.
**Do This:** Use environment variables, configuration files (EDN, YAML), or a configuration management tool like Consul or etcd to manage application settings. Use integrant or component libraries to handle the system state changes.
**Don't Do This:** Hard-code configuration values or store sensitive information in code.
**Why:** Configuration management provides flexibility and prevents the need to redeploy the application when configuration changes.
**Example using environment variables:**
"""clojure
(def db-url (or (System/getenv "DATABASE_URL") "default-db-url"))
"""
### 2.6. Security
Implement security best practices to protect your application and data.
**Do This:** Implement authentication and authorization. Use HTTPS for all communication. Sanitize user input to prevent injection attacks. Keep dependencies up to date. Use a static analysis tool like "clj-kondo" and a security analysis tool such as "lein-security" (though the latter has not been updated in sometime, review its suitability carefully before use).
**Don't Do This:** Store passwords in plain text or neglect regular security audits.
**Why:** Security measures protect your application from common vulnerabilities and ensure data confidentiality and integrity.
### 2.7. Database connection pooling
Using a connection pool is essential for performance and stability.
**Do this:** Utilize a library like "hikari-cp" or "clojure.java.jdbc" with connection pooling configuration.
**Don't do this:** Create database connections for each query; this is extremely resource-intensive.
**Why:** Connection pools pre-establish a set of database connections, avoiding the overhead of creating a new connection for each query.
**Example using "hikari-cp" inside Integrant:**
"""clojure
(ns my-app.db
(:require
[hikari-cp.core :as hikari]
[integrant.core :as ig]))
(defmethod ig/init-key ::db [key config]
(println "Connecting to database...")
(hikari/make-datasource config))
(defmethod ig/halt-key! ::db [datasource]
(println "Closing database connection pool...")
(hikari/close-datasource datasource))
;; Example Configuration
{:db {:jdbcUrl "jdbc:postgresql://localhost:5432/mydatabase"
:username "user"
:password "password"
:poolMaximumLeaseLifetime 1800000
:poolMaximumIdleTime 600000
:minimumIdle 5
:maximumPoolSize 20}}
"""
This example demonstrates how to integrate "hikari-cp" with Integrant to manage the database connection pool lifecycle. Similar setup can be achieved using the Component library.
### 2.8. Idempotency
Ensure operations are idempotent, particularly in distributed systems.
**Do this:** Design critical operations to be idempotent.
**Don't do this:** Assume that an operation will be executed only once.
**Why:** Because failures can happen at any time in distributed systems, its necessary to be able to safely retry operations without unintended side effects. A classic example is applying payment to an account. It should be possible to retry that operation without accidentally double-charging the user.
## 3. Modern Approaches and Patterns
Modern Clojure deployments leverage new features and approaches to improve efficiency and reliability.
### 3.1. GraalVM Native Image
GraalVM Native Image compiles Clojure code into standalone executables, resulting in significantly faster startup times and reduced memory footprint.
**Do This:** Evaluate GraalVM Native Image compilation for services with strict performance requirements especially where extremely fast startup times are needed (e.g. serverless functions).
**Don't Do This:** Blindly adopt GraalVM without considering potential compatibility issues with third-party libraries or increased build complexity. Testing is paramount.
**Why:** Native Image compilation excels when extremely low startup latency (< 100ms) is important. This comes at tradeoffs in the form of ahead-of-time compilation which can be quite slow especially in environments with limited RAM.
### 3.2. Serverless Deployments
Deploy Clojure applications to serverless platforms like AWS Lambda, Google Cloud Functions, or Azure Functions.
**Do This:** Optimize code for cold starts by minimizing dependencies and AOT compiling critical namespaces, or make use of container images.
**Don't Do This:** Deploy large, monolithic applications to serverless platforms without considering cold start penalties.
**Why:** Serverless deployments provide scalability, cost-effectiveness, and reduced operational overhead.
### 3.3. Observability
Implement comprehensive logging, tracing, and metrics collection for enhanced insight into application behavior.
**Do This:** Use distributed tracing tools like Jaeger or Zipkin to track requests across multiple services. Collect and analyze application metrics using Prometheus and Grafana. Log everything that may come into use later for debugging.
**Don't Do This:** Neglect tracing or rely solely on logging for troubleshooting.
**Why:** Observability provides a holistic view of application performance, enabling faster identification and resolution of issues.
This comprehensive guide provides a foundation for establishing and enforcing consistent Clojure deployment and DevOps standards within your team. By adhering to these guidelines, you can improve code quality, ensure reliable deployments, and optimize operational efficiency. Remember to adapt these standards to fit specific project needs and technology stacks.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Testing Methodologies Standards for Clojure This document outlines the standards for testing methodologies in Clojure projects. These standards promote maintainability, reliability, and code quality. They are designed to be used in conjunction with AI coding assistants such as GitHub Copilot and Cursor to promote adherence to these guidelines. ## 1. General Principles ### 1.1. Prioritize Testing * **Do This:** Treat testing as a first-class citizen in the development process. Write tests concurrently with, or even before, writing the function or module implementation itself. * **Don't Do This:** Neglect writing tests or consider it an "afterthought." * **Why:** Starting with tests (TDD – Test-Driven Development) fosters better design, reduces debugging time, and improves confidence in the code's correctness. ### 1.2. Test Pyramid * **Do This:** Structure tests in a pyramid shape: many unit tests, fewer integration tests, and even fewer end-to-end tests. * **Don't Do This:** Rely heavily on slow and brittle end-to-end tests at the expense of focused unit tests. * **Why:** Unit tests are fast, cheap to maintain, and provide quick feedback. Integration tests verify interactions between components, while end-to-end tests validate the entire system but are slower and more complex. ### 1.3. Test Isolation * **Do This:** Ensure tests are independent. Each test should set up its own context and tear it down after execution. * **Don't Do This:** Share mutable state between tests or rely on test execution order. * **Why:** Isolated tests ensure that failures are reproducible and easier to diagnose. They also prevent tests from interfering with each other. ### 1.4. Use Meaningful Assertions * **Do This:** Write assertions that clearly express the expected behavior. Use descriptive messages to communicate the purpose and context of the assertion. * **Don't Do This:** Use opaque or generic assertions that provide little insight into the underlying problem when a test fails. * **Why:** Clear assertions make it easier to understand what the test is verifying and aid in debugging when a test fails. ## 2. Unit Testing ### 2.1. Focus * **Do This:** Unit tests should verify the behavior of a single, isolated unit of code (usually a function or a small set of functions). * **Don't Do This:** Write unit tests that test multiple units of code or depend on external systems. * **Why:** Isolating units of code allows for precise bug identification; issues can be narrowed down and addressed at the foundational module level. ### 2.2. Mocking and Stubbing * **Do This:** Use mocking or stubbing to isolate the unit under test from its dependencies. Libraries like "clojure.test.check.clojure-test" and "mockfn" can be helpful. * **Don't Do This:** Directly test dependencies that are not part of the unit being tested. * **Why:** Isolating dependencies prevents failures in those dependencies from masking failures in the unit under test. It also leads to faster and more reliable tests. #### 2.2.1 Example with "mockfn" """clojure (ns my-project.core-test (:require [clojure.test :refer :all] [my-project.core :as core] [mockfn.clojure-test :refer [with-mock]])) (deftest test-calculate-total (testing "Calculate total with mocked discount" (with-mock [core/apply-discount (fn [price discount] 5.0)] ; Mock apply-discount function (let [items [{:price 10} {:price 20}] total (core/calculate-total items 0.2)] (is (= 100 total) "Total should be correct after applying mocked discount")))) (testing "Calculate total without mocks" (let [items [{:price 10} {:price 20}] total (core/calculate-total items 0.2)] (is (= 24 total) "Total should be correct without mocks")))) """ ### 2.3. Property-Based Testing * **Do This:** Use property-based testing, with libraries like "clojure.test.check", to automatically generate test data and verify that your functions satisfy certain properties. * **Don't Do This:** Rely solely on example-based testing (i.e., providing fixed inputs and outputs). * **Why:** Property-based testing can uncover edge cases and corner cases that are easily missed with example-based testing. #### 2.3.1 Example with "clojure.test.check" """clojure (ns my-project.core-test (:require [clojure.test :refer :all] [clojure.test.check.clojure-test :refer [defspec]] [clojure.test.check.properties :as prop] [clojure.test.check.generators :as gen] [my-project.core :as core])) (defspec square-non-negative 1000 ; Number of iterations (prop/for-all [x gen/nat] ; Generate natural numbers (>= (core/square x) 0))) ; Property: Square is non-negative """ In this example, "defspec" defines a property-based test named "square-non-negative". It uses the "gen/nat" generator to create random non-negative integers and verifies that the "square" function always returns a non-negative result. This tests a general property of "square", ensuring all non-negative results are >= 0. ### 2.4. Test Naming * **Do This:** Use descriptive names for tests that clearly indicate the scenario being tested. * **Don't Do This:** Use vague or ambiguous test names. * **Why:** Descriptive test names make it easier to understand the purpose of the test and to quickly identify failing tests. ### 2.5. Edge Cases * **Do This:** Always consider edge cases, boundary conditions, and invalid inputs when writing unit tests. * **Don't Do This:** Only test happy paths. * **Why:** Testing edge cases increases the robustness and reliability of your code. ## 3. Integration Testing ### 3.1. Focus * **Do This:** Integration tests should verify the interactions between two or more units of code. * **Don't Do This:** Use integration tests to test individual units in isolation (use unit tests for that). * **Why:** Integration tests ensure that components work together correctly and that data flows smoothly between them. ### 3.2. Database Interactions * **Do This:** Use a dedicated testing database for integration tests involving databases. * **Don't Do This:** Run integration tests against a production database. * **Why:** Using a separate testing database prevents tests from corrupting production data and ensures that tests are repeatable. ### 3.3. External APIs * **Do This:** Use mock services or stubs for external APIs when integration testing. Libraries like "clojure.test.check.clojure-test" alongside "mockfn" can be used. * **Don't Do This:** Directly call external APIs during integration tests, especially for APIs that might have rate limits or costs. * **Why:** Mocking external APIs provides control over the responses and eliminates dependencies on external systems, providing faster and more reliable tests. #### 3.3.1 Mocking External APIs with Component This is an example of mocking external APIs during integrations tests: """clojure (ns my-project.integration-test (:require [clojure.test :refer :all] [com.stuartsierra.component :as component] [my-project.system :as system] [my-project.api :as api])) (defn mock-api-component [value] (reify api/API (fetch-data [_ _] value))) (defn create-test-system [mock-api-value] (component/system-map :config (system/create-config) :api (mock-api-component mock-api-value) ; Use the mock API :service (system/create-service))) (deftest test-integration-with-mocked-api (let [test-system (component/start (create-test-system {:example "data"})) ; Provide desired return value service (:service test-system)] (try (is (= {:example "data"} (api/fetch-data (:api test-system) :some-param)) "Data fetched should match mock") (finally (component/stop test-system))))) """ ### 3.4. State Management * **Do This:** Carefully manage state during integration tests. Ensure that state is reset before each test. * **Don't Do This:** Allow state to accumulate between tests. * **Why:** Inconsistent state can lead to unpredictable test results and make it difficult to isolate bugs. ## 4. End-to-End (E2E) Testing ### 4.1. Focus * **Do This:** End-to-end tests should verify the entire system from the user's perspective. * **Don't Do This:** Use E2E tests to verify individual components or interactions (use unit and integration tests for that). * **Why:** E2E tests ensure that the system as a whole functions correctly and delivers the expected user experience. ### 4.2. Test Environment * **Do This:** Use a dedicated testing environment for E2E tests that closely resembles the production environment. * **Don't Do This:** Run E2E tests against a development environment or a shared testing environment. * **Why:** A dedicated testing environment provides a more realistic and reliable testing experience. ### 4.3. Automation * **Do This:** Automate E2E tests to ensure they can be run frequently and consistently. Use libraries like "selenium" or "playwright," wrapped as necessary. * **Don't Do This:** Manually run E2E tests. * **Why:** Automated E2E tests reduce the risk of human error and provide faster feedback on system-level issues. ### 4.4. Data Setup * **Do This:** Carefully manage test data for E2E tests. Ensure that data is set up and cleaned up before and after each test. * **Don't Do This:** Rely on existing data in the testing environment or leave data behind after tests are run. * **Why:** Consistent data management improves the reliability and repeatability of E2E tests. ### 4.5. Performance * **Do This:** Be mindful of the performance impact of E2E tests. Optimize tests to minimize execution time. Libraries like "perforate" or "criterium" can be used. * **Don't Do This:** Write E2E tests that are slow or inefficient. * **Why:** Fast E2E tests provide quicker feedback and reduce the overall testing time. ## 5. Tools and Libraries ### 5.1. "clojure.test" * Use the built-in "clojure.test" library for basic testing functionality. """clojure (ns my-project.core-test (:require [clojure.test :refer :all] [my-project.core :as core])) (deftest test-add (testing "Adding two positive numbers" (is (= 3 (core/add 1 2)))) (testing "Adding a positive and a negative number" (is (= -1 (core/add 1 -2))))) """ ### 5.2. "clojure.test.check" * Use "clojure.test.check" for property-based testing. """clojure (ns my-project.core-test (:require [clojure.test :refer :all] [clojure.test.check.clojure-test :refer [defspec]] [clojure.test.check.properties :as prop] [clojure.test.check.generators :as gen] [my-project.core :as core])) (defspec add-commutative 1000 (prop/for-all [x gen/nat y gen/nat] (= (core/add x y) (core/add y x)))) """ ### 5.3. "mockfn" * Use "mockfn" for mocking function calls during unit tests. """clojure (ns my-project.core-test (:require [clojure.test :refer :all] [my-project.core :as core] [mockfn.clojure-test :refer [with-mock]])) (deftest test-calculate-total (testing "Calculate total with mocked discount" (with-mock [core/apply-discount (fn [price discount] 5.0)] (let [items [{:price 10} {:price 20}] total (core/calculate-total items 0.2)] (is (= 100 total)))))) """ ### 5.4. "component" Testing * When working with components, tests often need to start and stop systems, mocking dependencies for isolation. """clojure (ns my-project.component-test (:require [clojure.test :refer :all] [com.stuartsierra.component :as component] [my-project.system :as system])) (deftest test-system-startup (let [test-system (component/start (system/new-system))] (try (is (not (nil? (:database test-system))) "Database should be started") (is (not (nil? (:web-server test-system))) "Web server should be started") (finally (component/stop test-system))))) """ ### 5.5 "stateful-testing" * For situations requiring in memory mocking instead of calls to external dependencies """clojure (ns my-project.state-test (:require [clojure.test :refer :all] [my-project.state :as state])) (deftest test-add-to-state (testing "Adding an item to the state" (state/reset-state) ; Reset the state before the test (state/add-item "item1") (is (= #{"item1"} @state/current-state) "State should contain the added item")) (testing "Adding another item to the state" (state/add-item "item2") (is (= #{"item1" "item2"} @state/current-state) "State should contain both items"))) """ ## 6. Test-Driven Development (TDD) ### 6.1. Red-Green-Refactor * **Do This:** Follow the Red-Green-Refactor cycle of TDD: * **Red:** Write a failing test. * **Green:** Write the minimum amount of code to make the test pass. * **Refactor:** Improve the code while keeping the test passing. * **Don't Do This:** Write code before writing tests or skip the refactoring step. * **Why:** TDD leads to better-designed code, reduces the risk of over-engineering, and increases confidence in the code's correctness. ### 6.2. Test Coverage * **Do This:** Aim for high test coverage (e.g., 80% or higher), but don't treat it as the sole metric of code quality. Libraries exist to determine line by line test coverage. * **Don't Do This:** Focus solely on achieving a certain test coverage percentage without considering the quality of the tests. * **Why:** High test coverage reduces the risk of regressions and makes it easier to maintain the code. However, meaningful tests count more than lines of code. ## 7. Reporting and CI/CD ### 7.1. Test Reporting * **Do This:** Use a test runner that provides detailed test reports with information about failures, errors, and code coverage. Use a variety of reporting formats, to allow multiple automated tools to process the results. * **Don't Do This:** Rely on manual inspection of test output. * **Why:** Automated test reporting makes it easier to track test results and identify potential issues. ### 7.2. Continuous Integration (CI) * **Do This:** Integrate tests into a CI/CD pipeline so that they are run automatically whenever code is changed. * **Don't Do This:** Run tests manually as part of the deployment process. * **Why:** CI/CD pipelines provide early feedback on code quality and ensure that changes are thoroughly tested before being deployed. ## 8. Code Examples ### 8.1. Asynchronous Testing When your Clojure code deals with asynchronous operations (e.g., using "core.async" or futures), testing requires special care to handle timing issues. """clojure (ns my-project.async-test (:require [clojure.test :refer :all] [clojure.core.async :as async :refer [>!! <!! timeout]] [my-project.async-operations :as ops])) (deftest test-async-operation (testing "Async operation completes successfully" (let [result-chan (async/chan) _ (ops/perform-async-task result-chan)] (let [result (<!! (async/timeout 1000) result-chan)] ; Timeout after 1 second (is (= :success result) "Async operation should return :success"))))) """ ### 8.2. Testing Exception Handling Ensuring that your code handles exceptions gracefully is critical. """clojure (ns my-project.exception-test (:require [clojure.test :refer :all] [my-project.error-handling :as eh])) (deftest test-divide-by-zero (testing "Divide by zero throws exception" (is (thrown? ArithmeticException (eh/divide 10 0))))) (deftest test-recover-from-exception (testing "Recovering from an exception using try-catch" (is (= :recovered (eh/recoverable-operation))))) """ ## 9. Conclusion Adhering to these testing standards will lead to more maintainable, reliable, and robust Clojure applications. Use these guidelines in tandem with AI coding assistants to promote consistent and high-quality testing practices within your development team. Regularly review and update these standards to align with the evolving Clojure ecosystem and your project's specific needs.
# Component Design Standards for Clojure This document outlines the component design standards for Clojure, focusing on creating reusable, maintainable, and performant components. It is intended to guide developers and inform AI coding assistants to produce idiomatic and high-quality Clojure code. It is specifically tailored to component design patterns in Clojure, and should be viewed as a single rule in a larger set of standards. ## 1. General Principles ### 1.1. Definition of a Component In Clojure, a component is a self-contained unit of functionality that encapsulates data and behavior. Components should be designed to be composable, independent, and easily testable. They should have clear, well-defined interfaces and responsibilities. ### 1.2. Key Principles of Component Design * **Single Responsibility Principle (SRP):** Each component should have one, and only one, reason to change. * **Open/Closed Principle (OCP):** Components should be open for extension but closed for modification. * **Liskov Substitution Principle (LSP):** Subtypes must be substitutable for their base types without altering the correctness of the program. While less directly applicable in Clojure with its focus on data and protocols, this principle translates to ensuring that functions accepting one type of data can also reliably handle variations or extensions of that data. * **Interface Segregation Principle (ISP):** Clients should not be forced to depend on methods they do not use. In Clojure, this suggests favoring small, focused protocols over large, monolithic ones. * **Dependency Inversion Principle (DIP):** High-level modules should not depend on low-level modules. Both should depend on abstractions. ### 1.3. Why These Principles Matter These principles are crucial for: * **Maintainability:** Easier to understand, modify, and debug components. * **Reusability:** Components can be used in different parts of the application or in different applications altogether. * **Testability:** Independent components are easier to test in isolation. * **Scalability:** Well-designed components facilitate easier scaling and distribution of the application. ## 2. Component Composition Approaches ### 2.1. Function Composition Clojure's functional nature makes function composition a natural way to build components. **Do This:** * Use "comp" to combine functions into a new function. * Leverage transducers for efficient data transformations. **Don't Do This:** * Create overly complex, deeply nested function compositions that are hard to read. * Depend excessively on mutable state within composed functions. **Example:** """clojure (defn add-one [x] (+ x 1)) (defn square [x] (* x x)) ; Compose functions to square a number and then add one (def square-and-add-one (comp add-one square)) (println (square-and-add-one 5)) ; Output: 26 ; Using transducers for efficient composition (def increment-and-square (comp (map inc) (map #(* % %)))) (println (into [] increment-and-square [1 2 3])) ; Output: [4 9 16] """ **Explanation:** Function composition allows combining simple functions into more complex operations. Transducers enhance this by providing a way to compose transformations on collections without creating intermediate collections. ### 2.2. Data-Driven Composition Components can be designed to operate on data structures. **Do This:** * Use data structures (maps, vectors, sets) as the primary means of communication between components. * Define functions that operate on these data structures to perform specific tasks. * Use schema libraries like "clojure.spec.alpha" or "malli" to define the structure and validity of your data. **Don't Do This:** * Pass large, mutable stateful objects between components. * Rely on side effects to communicate between components. **Example:** """clojure (require '[clojure.spec.alpha :as s]) ; Define a spec for a user (s/def ::user (s/keys :req-un [::id ::name ::email])) (s/def ::id uuid?) (s/def ::name string?) (s/def ::email string?) ; A function to validate a user (defn validate-user [user] (s/valid? ::user user)) ; A function to format a user's name (defn format-user-name [user] (str "User: " (:name user))) (def user-data {:id (random-uuid) :name "Alice" :email "alice@example.com"}) (println (validate-user user-data)) ; Output: true (println (format-user-name user-data)) ; Output: User: Alice """ **Explanation:** This approach emphasizes data structures as the central point of interaction. Using schemas further solidifies the interfaces between components, making them more robust and easier to reason about. ### 2.3. Protocol-Based Composition Protocols define interfaces that components can implement. This allows for polymorphism and extensibility. **Do This:** * Define protocols that represent the capabilities of a component. * Implement these protocols for different data types or data structures. * Use protocols to abstract away implementation details. **Don't Do This:** * Create overly large protocols with many methods. * Violate the Liskov Substitution Principle by implementing protocols inconsistently. **Example:** """clojure (defprotocol Printable (to-string [this] "Converts the object to a string representation.")) (extend-protocol Printable java.lang.String (to-string [this] this) ; Strings are already printable clojure.lang.PersistentVector (to-string [this] (str "[" (clojure.string/join ", " (map to-string this)) "]")) java.lang.Integer (to-string [this] (str this))) (println (to-string "Hello")) ; Output: Hello (println (to-string [1 2 "World"])) ; Output: [1, 2, World] """ **Explanation:** Protocols enable polymorphism. Different data types can implement the same protocol, providing a consistent interface. "extend-protocol" provides a flexible way to add protocol implementations to existing types. ### 2.4. Component Libraries and Frameworks Several libraries and frameworks facilitate component-based development in Clojure. * **Component:** A library for managing component lifecycles (start, stop). * **Integrant:** A configuration-based system for building applications from components controlled by a data structure. * **Mount:** A simpler alternative to Component for managing application state. * **System:** A newer library aiming to combine the best aspects of Component and Integrant. **Do This:** * Choose a component library appropriate for your project's complexity. * Use the library's lifecycle management features to manage component dependencies and state. * Employ configuration-based systems like Integrant to externalize component configuration. **Don't Do This:** * Manually manage component lifecycles without a library. * Hardcode component dependencies within the component itself. **Example (using Integrant):** """clojure (ns my-app (:require [integrant.core :as ig])) ; Define a database component (defmethod ig/init-key ::db [_ {:keys [url]}] (println "Connecting to DB at" url) {:connection url}) ; In a real app, establish an actual DB connection (defmethod ig/halt-key ::db [_ db] (println "Disconnecting from DB at" (:connection db))) ; Close the connection ; Define an HTTP server component (defmethod ig/init-key ::server [_ {:keys [port db]}] (println "Starting server on port" port "using DB" db) {:port port :db db}) ; In a real app, start an HTTP server (defmethod ig/halt-key ::server [_ server] (println "Stopping server on port" (:port server))) ; Stop the server ; Define a configuration (def config {::db {:url "jdbc://localhost:5432/mydb"} ::server {:port 8080 :db (ig/ref ::db)}}) ; Start the system (def system (ig/init config)) ; Stop the system when done (ig/halt! system) """ **Explanation:** Integrant allows defining components (like "::db" and "::server") and their dependencies in a configuration map. "ig/init" starts the system, resolving dependencies using references (e.g., "(ig/ref ::db)"). "ig/halt!" shuts down the system in the reverse order. The "ig/init-key" and "ig/halt-key" multimethods define the initialization and termination logic for each component. This removes boilerplate from your component implementation and centralizes configuration. ## 3. Component Communication ### 3.1. Asynchronous Messaging For decoupled components, consider asynchronous messaging using libraries like "core.async" or message queues (e.g., RabbitMQ, Kafka). **Do This:** * Use channels ("core.async") or message queues for non-blocking communication. * Define clear message formats using data structures and schemas. **Don't Do This:** * Overuse asynchronous messaging when synchronous calls are sufficient. * Create complex message routing logic within components. **Example (using core.async):** """clojure (require '[clojure.core.async :as async]) ; Create a channel (def message-channel (async/chan)) ; Component 1 (producer) (defn send-message [message] (async/>!! message-channel message)) ; Blocking send ; Component 2 (consumer) (defn receive-message [] (async/<!! message-channel)) ; Blocking receive ; Example Usage (future (send-message "Hello from Component 1")) (println (receive-message)) ; Output: Hello from Component 1 """ **Explanation:** "core.async" provides channels for asynchronous communication. The "send-message" function sends a message to the channel, and "receive-message" retrieves it. The "future" macro runs the sender in a separate thread. Non-blocking versions of these operations are available (">!", "<!"). ### 3.2. Publish/Subscribe (Pub/Sub) For components that need to react to events, consider using a publish/subscribe pattern. Libraries like "clojure.tools.namespace.repl" use this pattern internally. **Do This:** * Use a dedicated pub/sub library (e.g., implementing a simple one with atoms and callbacks). * Define clear event types and data structures. **Don't Do This:** * Create tightly coupled event listeners. * Overuse global event buses. **Example (Simple Pub/Sub implementation):** """clojure (def event-bus (atom {})) (defn subscribe [event-type callback] (swap! event-bus update event-type (fn [callbacks] (conj (or callbacks []) callback)))) (defn publish [event-type event-data] (doseq [callback (get @event-bus event-type)] (callback event-data))) ; Example Usage (subscribe :user-created (fn [user] (println "User created:" user))) (subscribe :user-created (fn [user] (println "Sending welcome email to:" (:email user)))) (publish :user-created {:name "Bob" :email "bob@example.com"}) """ **Explanation:** This demonstrates a basic pub/sub system managed with an atom. "subscribe" registers a callback for a specific event type. "publish" triggers all callbacks associated with that event type, passing event data to each. ### 3.3. Services and APIs Expose component functionality as services or APIs using libraries like Ring/Compojure or shadow-cljs for frontend components. **Do This:** * Define clear API contracts using schemas or Swagger/OpenAPI. * Implement proper authentication and authorization. **Don't Do This:** * Expose internal component implementation details in the API. * Neglect security considerations. ## 4. Testing ### 4.1. Unit Testing Test individual components in isolation. **Do This:** * Use "clojure.test" or a testing framework like Midje. * Mock dependencies when necessary. * Write clear and concise test cases. **Don't Do This:** * Write tests that are tightly coupled to implementation details. * Neglect edge cases and error handling. **Example:** """clojure (ns my-app.core-test (:require [clojure.test :refer :all] [my-app.core :refer :all])) (deftest test-add-one (testing "Should add one to a number" (is (= (add-one 5) 6)) (is (= (add-one -1) 0)))) """ ### 4.2. Integration Testing Test how components interact with each other. **Do This:** * Test the entire system or a significant portion. * Use realistic test data. * Verify that components communicate correctly. **Don't Do This:** * Skip integration testing. * Assume that components work together correctly without testing. ### 4.3. Component Lifecycle Testing If using a component lifecycle library like "component" or "integrant", verify the start and stop behavior of components. **Do This:** * Write tests that start and stop components. * Verify that resources are acquired and released correctly. * Ensure that dependencies are started in the correct order. ## 5. Error Handling ### 5.1. Exceptions Use exceptions to signal exceptional conditions. **Do This:** * Throw exceptions when errors occur. * Catch exceptions at appropriate boundaries. * Provide informative error messages. **Don't Do This:** * Ignore exceptions. * Use exceptions for normal control flow. ### 5.2. Error Values Return error values (e.g., "nil", "false", or a tagged union) to indicate errors. **Do This:** * Use error values when exceptions are not appropriate. * Check for error values and handle them appropriately. **Don't Do This:** * Ignore error values. * Assume that all operations succeed. ### 5.3. Logging Log errors and other important events. **Do This:** * Use a logging library like "clojure.tools.logging". * Log at appropriate levels (e.g., "error", "warn", "info", "debug"). * Include relevant context in log messages. **Don't Do This:** * Log too much information. * Log sensitive information. ## 6. Modern Clojure Features ### 6.1. clojure.spec.alpha and Malli Use "clojure.spec.alpha" or "malli" extensively for data validation, generation, and documentation. **Do This:** * Define specs/schemas for all data structures used in your application. * Use "s/valid?" or "malli.core/validate" to validate data. * Use "s/gen" or "malli.core/generate" for property-based testing. **Don't Do This:** * Neglect data validation. * Assume that all data is valid. ### 6.2. Datafy/Nav Consider using "datafy" and "nav" (Clojure 1.10+) to provide a consistent way to inspect and navigate data structures. This can improve debugging and introspection. **Do This:** * Implement "datafy" and "nav" for custom data types. * Use the "datafy" function in debugging tools. ## 7. Anti-Patterns ### 7.1. God Object Avoid creating large, monolithic components that do too much. ### 7.2. Tight Coupling Minimize dependencies between components. ### 7.3. Premature Optimization Don't optimize components before they are needed. ### 7.4. Reinventing the Wheel Use existing libraries and frameworks when possible. ## 8. Performance Optimization ### 8.1. Immutability Leverage Clojure's immutable data structures for performance and concurrency benefits. ### 8.2. Laziness Use lazy sequences efficiently to avoid unnecessary computation. ### 8.3. Concurrency Use Clojure's concurrency features (e.g., atoms, refs, agents, core.async) to improve performance. Be extremely cautious and deliberate using these as they are easy to misuse. ### 8.4. Profiling Use profiling tools to identify performance bottlenecks. ## 9. Security Best Practices ### 9.1. Input Validation Validate all user input to prevent injection attacks. ### 9.2. Authentication and Authorization Implement proper authentication and authorization to protect sensitive data. ### 9.3. Dependency Management Keep dependencies up to date to patch security vulnerabilities. By adhering to these component design standards, Clojure developers can create applications that are maintainable, reusable, testable, and performant. This document provides a framework for building high-quality Clojure code.
# API Integration Standards for Clojure This document outlines the coding standards for API integration in Clojure projects. It aims to provide a comprehensive guide for developers to build robust, maintainable, and secure integrations with external services. These standards will help improve code quality, reduce errors, and facilitate collaboration. ## 1. Architectural Considerations for API Integration ### 1.1. Microservices and API Gateways * **Do This:** Embrace a microservices architecture where each service is responsible for a specific domain or functionality. Use an API gateway (e.g., using "ring-swagger" or "reitit") to handle routing, authentication, and rate limiting. The API Gateway acts as a single entry point for all client requests, decoupling backend services from the client applications. * **Don't Do This:** Avoid monolithic designs where all API integrations are crammed into a single service causing tight coupling, and making it difficult to scale or modify independently. * **Why:** Microservices allow independent scaling and deployment. API gateways centralize cross-cutting concerns like authentication and rate limiting enhancing security and usability. """clojure ;; Example: Basic API Gateway routing with Reitit (require '[reitit.ring :as ring]) (require '[reitit.coercion.spec :as rcs]) (require '[reitit.ring.coercion :as rrc]) (require '[reitit.ring.middleware.muuntaja :as rrmm]) (require '[muuntaja.core :as m]) (require '[reitit.ring.middleware.exception :as exception]) (require '[ring.adapter.jetty :as jetty]) (defn hello-handler [request] {:status 200 :body {:message "Hello, World!"}}) (def routes [["/hello" {:get {:handler hello-handler}}]]) (def app (ring/ring-handler (ring/router routes {:data {:coercion rcs/coercion :muuntaja m/instance :middleware [rrmm/format-middleware rrc/coercion-middleware exception/exception-middleware]}}) (ring/create-default-handler {:not-found (fn [_] {:status 404, :body "Not found"})}))) (defn start-server [] (jetty/run-jetty app {:port 3000 :join? false})) """ ### 1.2. Asynchronous Communication * **Do This:** Utilize asynchronous messaging queues (e.g., Kafka, RabbitMQ, Amazon SQS) for non-critical API interactions to decouple services and improve resilience. Employ "core.async" for managing concurrency within Clojure services. Handle failures and retries gracefully. * **Don't Do This:** Rely solely on synchronous HTTP requests, especially for long-running operations, this can lead to performance bottlenecks and decreased system responsiveness. * **Why:** Asynchronous communication promotes loose coupling, improves scalability, and enhances system resilience by allowing services to operate independently. """clojure ;; Example: Using Manifold for asynchronous HTTP requests (require '[manifold.stream :as s] '[manifold.deferred :as d] '[aleph.http :as http]) (defn fetch-data [url] (-> (http/get url {:accept "application/json"}) (d/chain :body) ; Extract the body from the response (d/catch (fn [e] (println "Error fetching data from" url ":" (.getMessage e)) nil)))) (defn process-data [deferred-data] (d/chain deferred-data (fn [data] (if data (do (println "Processing data:" (count data) "bytes") (try (let [parsed-data (cheshire.core/parse-string data true)] ; Properly parse as JSON (println "Parsed Data:" (keys parsed-data)) parsed-data) (catch Exception e (println "Error parsing JSON:" (.getMessage e)) nil))) (println "No data to process"))))) (defn main [] (let [url "https://jsonplaceholder.typicode.com/todos/1" ; Use a stable and reliable API endpoint data-deferred (fetch-data url)] (process-data data-deferred))) (comment (main)) """ ### 1.3. Rate Limiting and Throttling * **Do This:** Implement rate limiting and throttling mechanisms to protect your APIs and backend services from overuse or abuse. Use libraries like "clj-throttle" or integrate with cloud provider rate limiting services. Clearly define the throttling limits and communicate these to API clients. * **Don't Do This:** Neglect rate limiting, leaving your services vulnerable to denial-of-service attacks and resource exhaustion. * **Why:** Rate limiting ensures fair usage of your APIs and protects your infrastructure from overload or malicious attacks. """clojure ;; Example: Basic rate limiting using "clj-throttle" (require '[clj-throttle.core :as throttle]) (def api-calls (atom 0)) (def rate-limiter (throttle/throttle 5 60000)) ; 5 calls per minute (60000ms) (defn handle-api-request [] (if (throttle/allowed? rate-limiter) (do (swap! api-calls inc) (println "API Request processed. Call count:" @api-calls)) (do (println "Rate limit exceeded. Try again later.") {:status 429 :body "Too Many Requests"}))) (comment (dotimes [_ 10] (handle-api-request)) ) """ ## 2. Data Serialization and Deserialization ### 2.1. JSON Handling * **Do This:** Employ "cheshire" or "data.json" for JSON serialization/deserialization. Configure these libraries for optimal performance. Consider using ":decode-key-fn" with cheshire to automatically convert keys to keywords. Specify encoding to UTF-8 when producing or consuming JSON. * **Don't Do This:** Roll your own JSON parsing logic or rely on inefficient or outdated libraries. Fail to handle potential parsing exceptions. * **Why:** "cheshire" and "data.json" are efficient, well-maintained libraries optimized for Clojure data structures. Proper error handling is crucial for robustness. """clojure ;; Example: Using Cheshire for JSON processing (require '[cheshire.core :as json]) (def data {:name "John Doe" :age 30 :city "New York"}) ;; Serialize data to JSON (def json-string (json/encode data)) (println json-string) ; Output: {"name":"John Doe","age":30,"city":"New York"} ;; Deserialize JSON string to Clojure data structure (def parsed-data (json/decode json-string true)) ; "true" converts keys to keywords (println parsed-data) ; Output: {:name "John Doe", :age 30, :city "New York"} ;; Example with :decode-key-fn (def parsed-data-keyword (json/decode json-string keyword)) (println parsed-data-keyword) ; Output: {:name "John Doe", :age 30, :city "New York"} ;;Proper error handling (try (json/decode "invalid json" true) (catch Exception e (println "Error decoding JSON:" (.getMessage e)))) """ ### 2.2. Data Coercion and Validation * **Do This:** Use "spec" or "malli" for data validation and coercion before sending or after receiving data from external APIs. Define clear schemas for API requests and responses to enforce consistency and catch errors early. * **Don't Do This:** Trust external data without validation, which could lead to runtime errors, security vulnerabilities, or data corruption. * **Why:** Validation ensures that data conforms to expected formats and constraints, reducing the risk of unexpected errors and security vulnerabilities, and ensures data consistency across systems. """clojure ;; Example: Using "spec" for data validation (require '[clojure.spec.alpha :as s]) ;; Define a spec for a user (s/def ::name string?) (s/def ::age (s/and int? #(>= % 0))) (s/def ::user (s/keys :req-un [::name ::age])) (def valid-user {:name "Alice" :age 25}) (def invalid-user {:name "Bob" :age -5}) (println "Valid user:" (s/valid? ::user valid-user)) ; Output: Valid user: true (println "Invalid user:" (s/valid? ::user invalid-user)) ; Output: Invalid user: false (println (s/explain-data ::user invalid-user)) """ ### 2.3. Handling Different Data Formats * **Do This:** Use libraries like "muuntaja" to handle various content types (JSON, XML, edn, etc.) in a consistent way. Specify content negotiation strategies when interacting with APIs that support multiple formats. * **Don't Do This:** Assume all APIs use the same data format. Hardcode format-specific parsing logic without considering content negotiation. * **Why:** "muuntaja" simplifies content negotiation and serialization/deserialization across different data formats, promoting flexibility and interoperability. """clojure ;; Example using muuntaja (require '[muuntaja.core :as m]) ;; Create a Muuntaja instance with support for JSON and EDN (def muuntaja (m/create)) (def data {:message "Hello, Muuntaja!"}) ;; Encode data to JSON (def json-bytes (m/encode muuntaja "application/json" data)) (println (String. json-bytes "UTF-8")) ; Output: {"message":"Hello, Muuntaja!"} ;; Decode JSON bytes to Clojure data (def decoded-data (m/decode muuntaja "application/json" json-bytes)) (println decoded-data) ; Output: {:message "Hello, Muuntaja!"} """ ## 3. HTTP Client Configuration ### 3.1. Choosing an HTTP Client * **Do This:** Prefer "clj-http", "hato", or "aleph" for making HTTP requests. Use "clj-http" for simple use-cases. Use "hato" for newer projects, and "aleph" for asynchronous, high-performance applications. Choose a client based on the project's needs and performance requirements. * **Don't Do This:** Use Java's built-in "java.net.URL" directly, as it lacks advanced features and is less convenient for modern API interactions. Overlook dependency vulnerabilities in HTTP client libraries. * **Why:** Dedicated HTTP client libraries offer features like connection pooling, request timeouts, and robust error handling, enhancing performance and reliability. ### 3.2. Request and Response Handling * **Do This:** Set appropriate request timeouts to prevent indefinite blocking. Handle different HTTP status codes gracefully (e.g., 2xx, 4xx, 5xx). Implement retry logic for transient errors (e.g., network glitches). Log both request and response details for debugging. * **Don't Do This:** Ignore HTTP status codes or fail to handle potential network errors. Implement naive retry logic that could worsen the situation (e.g., retry immediately and endlessly). * **Why:** Proper error handling and retry mechanisms improve the resilience of your API integrations. Detailed logging aids in diagnosing issues. """clojure ;;Example using "hato" (require '[hato.client :as client]) (defn make-api-call [url] (try (let [{:keys [status body] :as response} (client/get url {:accept "application/json" :socket-timeout 5000 :connect-timeout 5000})] (println "API Response:" response) (case status 200 (cheshire.core/parse-string body true) 404 {:error "Resource not found"} (throw (ex-info "API Error" {:status status :body body})))) ; Improved exception handling (catch Exception e (println "Error making API call:" (.getMessage (ex-data e))) {:error (.getMessage (ex-data e))}) (finally (client/close {})))) (defn retry-api-call [url retries] (loop [attempts 0] (if (> attempts retries) {:error "Max retries exceeded"} (let [result (make-api-call url)] (if (:error result) (do (println "Retrying in 2 seconds...") (Thread/sleep 2000) (recur (inc attempts))) result))))) (comment (def data (retry-api-call "https://jsonplaceholder.typicode.com/todos/1" 3)) (println data)) """ ### 3.3. Connection Pooling * **Do This:** Ensure your HTTP client library utilizes connection pooling to reuse connections and reduce latency, especially for frequent API calls. Configure connection pool size appropriately based on anticipated load. For "hato", this is managed automatically. With "clj-http" make sure you're reusing the same client instance across calls. * **Don't Do This:** Create a new HTTP client instance for every API call, which wastes resources and increases latency. * **Why:** Connection pooling improves performance by reducing the overhead associated with establishing new connections for each API interaction. ## 4. Authentication and Authorization ### 4.1. Secure Storage of Credentials * **Do This:** Store API keys, tokens, and other sensitive credentials securely using environment variables, encrypted configuration files (e.g., using "environ"), or a dedicated secret management service (e.g., HashiCorp Vault, AWS Secrets Manager). *Never* commit secrets directly to version control. * **Don't Do This:** Hardcode credentials in your code or store them in plain text configuration files. Expose credentials in logs or error messages. * **Why:** Secure storage of credentials is crucial to prevent unauthorized access to your APIs and backend systems. """clojure ;; Example using "environ" to fetch API key from environment variable (require '[environ.core :refer [env]]) (def api-key (env :my-api-key)) (if api-key (println "API Key:" api-key) (println "API Key not found in environment variables")) """ ### 4.2. Authentication Methods * **Do This:** Use appropriate authentication methods like OAuth 2.0, JWT, or API key authentication. Implement proper token validation. Support TLS/SSL (HTTPS) for all API communication to encrypt data in transit. * **Don't Do This:** Rely on weak or outdated authentication methods (e.g., Basic Auth without SSL). Skimp on certificate validation, which could expose your application to man-in-the-middle attacks. Use "http" instead of "https" for sensitive data transmission. * **Why:** Strong authentication and encryption are essential to protect against unauthorized access and data breaches. ### 4.3. Authorization * **Do This:** Enforce proper authorization checks to ensure that users only have access to the resources and operations they are permitted to access. Implement role-based access control (RBAC) or attribute-based access control (ABAC) if necessary. * **Don't Do This:** Grant excessive permissions or skip authorization checks, which could lead to privilege escalation attacks. * **Why:** Authorization prevents unauthorized users from accessing sensitive data or performing restricted operations. ## 5. Error Handling and Monitoring ### 5.1. Centralized Error Handling * **Do This:** Implement a centralized error handling mechanism using "try"/"catch"/"finally" blocks or a dedicated error logging library (e.g., "taoensso.timbre"). Log detailed error messages including relevant context for debugging. * **Don't Do This:** Swallow exceptions silently or propagate raw exceptions to the client without proper sanitization. * **Why:** Centralized error handling simplifies debugging and maintenance by providing a consistent way to manage and log errors. """clojure ;; Example: Centralized error handling with try/catch (require '[taoensso.timbre :as log]) (defn safe-api-call [url] (try (make-api-call url) (catch Exception e (log/error "API call failed:" (.getMessage e) (ex-data e)) {:error "An unexpected error occurred"}))) """ ### 5.2. Logging and Monitoring * **Do This:** Log all significant API interactions including requests, responses, and errors. Use structured logging formats (e.g., JSON) for easy analysis. Integrate with monitoring tools (e.g., Datadog, Prometheus) to track API performance, error rates, and other key metrics. * **Don't Do This:** Log sensitive data (e.g., passwords, API keys). Neglect monitoring, which makes it difficult to detect and diagnose issues. * **Why:** Logging and monitoring provide valuable insights into API usage, performance, and error patterns, enabling proactive issue detection and performance optimization. ### 5.3. Circuit Breaker Pattern * **Do This:** Implement the circuit breaker pattern using libraries like "hystrix-clj" or "failsafe" to prevent cascading failures when an external service becomes unavailable. The circuit breaker should automatically open when the error rate exceeds a certain threshold and close when the service recovers. * **Don't Do This:** Allow repeated calls to failing services which can worsen the system overload, Do not hardcode values. Use configuration. * **Why:** The circuit breaker pattern improves system resilience by isolating failing services and preventing them from bringing down the entire application. ## 6. Documentation and Testing ### 6.1. API Documentation * **Do This:** Document all APIs using tools like "ring-swagger" or "reitit-swagger". Include clear descriptions of API endpoints, request parameters, response formats, and error codes. Store your API spec as part of your code for versioning. * **Don't Do This:** Neglect API documentation, which makes it difficult for developers to understand and use your APIs. Keep API documentation separate from the codebase. * **Why:** API documentation is essential for enabling developers to effectively use your APIs. ### 6.2. Testing * **Do This:** Write unit tests and integration tests to verify the correctness and reliability of your API integrations. Use mocking libraries (e.g., "clojure.test.check.clojure-test") to isolate your code during testing. * **Don't Do This:** Skip testing or rely solely on manual testing, which can lead to undetected bugs. Write flaky tests making code review difficult. * **Why:** Thorough testing ensures that your API integrations function correctly and are resistant to errors. By following these coding standards, Clojure developers can create robust, maintainable, and secure API integrations that meet the needs of modern applications. This guide should promote code quality, collaboration, and reduce project risk.
# Security Best Practices Standards for Clojure This document outlines security best practices for Clojure development, designed to help developers write secure, maintainable, and performant code. It is intended to be used as a reference guide and a tool to configure AI coding assistants. This will provide a shared understanding of security principles and promote consistent application across projects. We will focus on the latest versions of Clojure and its ecosystem, addressing common vulnerabilities and secure coding patterns specific to the language. ## 1. General Security Principles ### 1.1 Principle of Least Privilege **Standard:** Grant only the minimum necessary permissions to users and applications. * **Do This:** Define fine-grained access control policies. * **Don't Do This:** Grant broad or administrative privileges unnecessarily. **Why:** Reduces the potential damage from compromised accounts or applications. **Example:** """clojure ;; Define roles and permissions (def roles {:admin #{:read :write :delete} :editor #{:read :write} :viewer #{:read}}) ;; Check permissions (defn has-permission? [user role permission] (contains? (get roles role #{}) permission)) ;; Example usage (if (has-permission? user :editor :write) (update-data data) (throw (ex-info "Insufficient permissions" {:user user :permission :write}))) """ ### 1.2 Defense in Depth **Standard:** Implement multiple layers of security controls. * **Do This:** Combine firewalls, intrusion detection systems, strong authentication mechanisms, and data encryption. * **Don't Do This:** Rely on a single security measure. **Why:** Ensures that if one layer fails, others remain in place to protect the system. **Example:** Layering security in a web application: 1. **Web Application Firewall (WAF):** Protects against common web attacks like SQL injection and XSS. 2. **Authentication:** Verify user identity with strong passwords and multi-factor authentication. 3. **Authorization:** Control access to specific resources based on user roles and permissions. 4. **Data Encryption:** Encrypt sensitive data at rest and in transit. 5. **Regular Security Audits:** Identify and address potential vulnerabilities. ### 1.3 Secure Configuration **Standard:** Securely configure all systems and applications. * **Do This:** Harden servers, disable unnecessary services, and use secure protocols like HTTPS. * **Don't Do This:** Use default passwords or insecure configurations. **Why:** Prevents attackers from exploiting misconfigurations to gain unauthorized access. **Example:** Securing a web server: * Disable default accounts and change default passwords. * Enable HTTPS and enforce TLS 1.3 or higher. * Configure firewalls to restrict access to necessary ports. * Regularly update software and apply security patches. * Use a configuration management tool like Ansible or Chef to automate secure configurations. ### 1.4 Input Validation and Sanitization **Standard:** Validate and sanitize all user input. * **Do This:** Use whitelists to define allowed input, encode special characters, and escape output. * **Don't Do This:** Trust user input without validation or sanitization. **Why:** Prevents injection attacks, such as SQL injection and cross-site scripting (XSS). This is especially true because of the dynamic nature of Clojure and the ease of creating dynamically executed forms. **Example:** """clojure (require '[clojure.string :as str]) (defn sanitize-string [s] (if (string? s) (-> s (str/replace #"<" "<") (str/replace #">" ">") (str/replace #"&" "&") (str/replace #"\"" """) (str/replace #"'" "'")) "")) (defn validate-email [email] (if (re-matches #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$" email) email nil)) (defn process-data [params] (let [email (validate-email (:email params)) name (sanitize-string (:name params))] (if (and email name) {:email email :name name} (throw (ex-info "Invalid input" {:params params})))) ) """ ### 1.5 Error Handling and Logging **Standard:** Implement proper error handling and logging mechanisms. * **Do This:** Log errors securely, handle exceptions gracefully, and avoid exposing sensitive information in error messages. * **Don't Do This:** Expose stack traces or internal details to users. **Why:** Provides insights into system behavior, helps identify and fix vulnerabilities, and prevents information leakage. **Example:** """clojure (require '[clojure.tools.logging :as log]) (defn process-data [data] (try ;; Process data here (log/info "Data processed successfully") :success (catch Exception e (log/error e "Error processing data") (throw (ex-info "Failed to process data" {:cause e})))) ) """ ## 2. Clojure-Specific Security Considerations ### 2.1 Code Injection **Standard:** Avoid using "eval" or similar functions that execute arbitrary code. * **Do This:** Use data structures and functions to manipulate code instead of generating and executing code. Use "read-string" with caution. * **Don't Do This:** Dynamically generate and execute code based on user input. **Why:** "eval" poses a significant security risk, as it allows attackers to execute arbitrary code on the server. Clojure macros provide a much safer and testable alternative in many cases. **Example:** Instead of using "eval", use data structures and functions: Bad: """clojure ;; DANGEROUS: Avoid this (defn process-expression [expression] (eval (read-string expression))) """ Good: """clojure ;; Safer approach using functions and data structures (defn add [x y] (+ x y)) (defn subtract [x y] (- x y)) (def operations {:add add :subtract subtract}) (defn safe-process-expression [operation x y] (if-let [op-fn (get operations operation)] (op-fn x y) (throw (ex-info "Invalid operation" {:operation operation})))) (safe-process-expression :add 5 3) ; Returns 8 """ ### 2.2 Denial of Service (DoS) **Standard:** Implement measures to prevent DoS attacks. * **Do This:** Limit request rates, use connection pooling, and set timeouts. * **Don't Do This:** Allow unlimited requests or resource consumption. **Why:** Protects the system from being overwhelmed by malicious traffic. **Example:** Using rate limiting with "ring-rate-limit": """clojure (require '[ring.middleware.rate-limit :refer [wrap-rate-limit]]) (def app (-> your-handler (wrap-rate-limit {:limit 1000 :window 60}))) ; 1000 requests per minute """ ### 2.3 Dependency Management **Standard:** Manage dependencies securely and keep them up-to-date. * **Do This:** Use a dependency management tool like "Leiningen" or "deps.edn". Regularly update dependencies to patch security vulnerabilities. * **Don't Do This:** Use outdated or untrusted dependencies. **Why:** Prevents vulnerabilities in third-party libraries from being exploited. **Example:** Managing dependencies with "deps.edn": """clojure {:deps {org.clojure/clojure {:mvn/version "1.11.1"} ring/ring-core {:mvn/version "1.9.6"} ;;CHECK DEPENDENCY VULNERABILITIES REGULARLY AND UPDATE }} """ Use tools like "lein ancient" or "clojure -A:deps tree" to check for outdated dependencies. Also, use tools like "owasp-dependency-check" to review libraries for known vulnerabilities. ### 2.4 Database Security **Standard:** Protect against SQL injection and other database vulnerabilities. * **Do This:** Use parameterized queries or ORM libraries to escape user input. Implement proper access control policies and encrypt sensitive data. * **Don't Do This:** Construct SQL queries directly from user input. **Why:** Prevents unauthorized access to and manipulation of data. **Example:** Using "clojure.java.jdbc" with parameterized queries: """clojure (require '[clojure.java.jdbc :as jdbc]) (def db-spec {:classname "org.postgresql.Driver" :subprotocol "postgresql" :subname "//localhost:5432/mydb" :user "myuser" :password "mypassword"}) (defn get-user [user-id] (jdbc/query db-spec ["SELECT * FROM users WHERE id = ?" user-id])) ;;Example: (get-user 123) """ ### 2.5 Session Management **Standard:** Implement secure session management practices. * **Do This:** Use strong session IDs, set appropriate session timeouts, and protect session data from tampering. Enforce HTTPS for session cookies. * **Don't Do This:** Use predictable session IDs or store sensitive data in session cookies. **Why:** Prevents session hijacking and unauthorized access to user accounts. **Example:** Using "ring.middleware.session" with secure options: """clojure (require '[ring.middleware.session :refer [wrap-session]]) (require '[ring.middleware.session.cookie :refer [cookie-store]]) (def app (-> your-handler (wrap-session {:store (cookie-store {:key "your-secret-key" :cookie-attrs {:http-only true, :secure true}})}))) """ ### 2.6 Cross-Site Scripting (XSS) **Standard:** Protect against XSS attacks. * **Do This:** Sanitize user input, encode output, and use Content Security Policy (CSP) headers. * **Don't Do This:** Trust user input without proper encoding. **Why:** Prevents attackers from injecting malicious scripts into web pages. **Example:** Using "hiccup" with auto-escaping and setting CSP headers: """clojure (require '[hiccup.core :as h]) (require '[ring.util.response :as response]) (defn display-data [data] (response/header (response/response (h/html [:div (h/escape-html data)])) "Content-Security-Policy" "default-src 'self'")) """ Utilize libraries like "clj-csp" to manage CSP headers more effectively. ### 2.7 Authentication and Authorization **Standard:** Implement strong authentication and authorization mechanisms. * **Do This:** Use strong passwords, multi-factor authentication, and role-based access control (RBAC). * **Don't Do This:** Store passwords in plain text or rely on weak authentication methods. **Why:** Ensures that only authorized users can access sensitive resources. **Example:** Using "buddy-auth" for authentication and authorization: """clojure (require '[buddy.auth.backends :as backends]) (require '[buddy.auth.middleware :as middleware]) (require '[buddy.auth.accessrules :as accessrules]) ;; Authentication backend (def backend (backends/jws {:secret "your-secret"})) ;; Access rules (def rules (accessrules/restrictive [[:admin] (accessrules/roles #{:admin})])) (def app (-> your-handler (middleware/wrap-authentication backend) (middleware/wrap-authorization rules))) """ ### 2.8 Secrets Management **Standard:** Securely manage sensitive information like API keys, passwords, and database credentials. * **Do This:** Store secrets in a secure vault (e.g., HashiCorp Vault, AWS Secrets Manager) and access them programmatically. Avoid storing secrets in code or configuration files. * **Don't Do This:** Hardcode secrets or store them in plaintext. **Why:** Prevents unauthorized access to sensitive information. **Example:** Using environment variables and a library to load secrets, rather than embedding them directly in the code. """clojure (defn get-secret [key] (System/getenv key)) (def db-password (get-secret "DB_PASSWORD")) (def db-spec {:classname "org.postgresql.Driver" :subprotocol "postgresql" :subname "//localhost:5432/mydb" :user "myuser" :password db-password}) """ For production environments, consider using libraries that integrate directly with secret management tools like Vault. ### 2.9 Data Encryption **Standard:** Encrypt sensitive data at rest and in transit. * **Do This:** Use strong encryption algorithms and protocols (e.g., AES-256, TLS 1.3). * **Don't Do This:** Use weak or outdated encryption methods. **Why:** Protects data from unauthorized access and disclosure. **Example:** Encrypting data using "friend" library: """clojure (require '[friend.crypto :as crypto]) (defn encrypt-data [data secret-key] (crypto/encrypt data secret-key)) (defn decrypt-data [encrypted-data secret-key] (crypto/decrypt encrypted-data secret-key)) (def secret-key "your-secret-key") (def data "sensitive data") (def encrypted-data (encrypt-data data secret-key)) (def decrypted-data (decrypt-data encrypted-data secret-key)) """ Ensure you handle the key securely, following secrets management best practices. ## 3. Security Tooling and Libraries ### 3.1 Static Analysis **Tools:** * **Klocwork:** Commercial static analysis tool that can identify security vulnerabilities and coding defects. * **Semgrep:** Open-source static analysis tool that supports custom rules for identifying security issues. * **clj-kondo:** Linter for Clojure code. While not explicitly a security tool, it can help in enforcing coding standards that reduce the risk of vulnerabilities. ### 3.2 Dependency Scanning **Tools:** * **OWASP Dependency-Check:** Detects known vulnerabilities in project dependencies. * **Snyk:** Commercial tool for identifying and remediating vulnerabilities in open-source dependencies. * "lein ancient": A Leiningen plugin to identify outdated dependencies. ### 3.3 Security Auditing * **Regular Security Audits:** Conduct regular security assessments and penetration testing to identify and address potential vulnerabilities. * **Code Reviews:** Ensure that code changes are reviewed by multiple developers to identify security issues. ## 4. Cloud-Specific Security If deploying to cloud environments, these security considerations are relevant: ### 4.1 Identity and Access Management (IAM) **Standard:** Utilize cloud provider's IAM services to manage access to resources. * **Do This:** Assign roles to users and services based on the principle of least privilege. * **Don't Do This:** Use root or administrative accounts for day-to-day operations. ### 4.2 Network Security **Standard:** Implement network security controls to isolate resources and restrict traffic. * **Do This:** Use Virtual Private Clouds (VPCs), security groups, and network ACLs to control network access. * **Don't Do This:** Expose resources directly to the internet without proper security controls. ### 4.3 Data Encryption **Standard:** Encrypt data at rest and in transit using cloud provider's encryption services. * **Do This:** Use KMS (Key Management Service) to manage encryption keys securely. * **Don't Do This:** Store encryption keys in code or configuration files. This document provides a foundation for building secure Clojure applications. By adhering to these standards and continuously improving security practices, development teams can minimize the risk of vulnerabilities and protect sensitive data. Continuous learning and adaptation to new threats and security technologies are crucial for maintaining a strong security posture.
# State Management Standards for Clojure This document outlines the standards and best practices for managing state in Clojure applications. It's intended to guide developers in creating maintainable, performant, and robust systems. These guidelines reflect current Clojure best practices and are applicable to modern Clojure development. ## 1. Principles of State Management in Clojure Clojure, as a functional programming language, emphasizes immutability. However, real-world applications require managing state. The key is to manage state in a controlled and predictable manner. * **Immutability as the Default:** Favor immutable data structures whenever possible. This simplifies reasoning about code, avoids side effects, and enables concurrency. * **Explicit State Management:** Make state transitions explicit and avoid implicit modification. This improves auditability and reduces the risk of unintended consequences. * **Controlled Concurrency:** Clojure provides powerful concurrency primitives (atoms, refs, agents, vars). Choose the appropriate primitive based on the specific concurrency requirements. * **Reactivity and Dataflow:** Consider using libraries like "integrant", Component, "re-frame", "Fulcro", or plain clojure.spec/watch for managing application lifecycle and data flow, especially in larger applications with complex dependencies and reactivity requirements. ## 2. Vars: Global Mutable State (Use with Caution!) Vars are the simplest mechanism for managing state in Clojure, providing thread-local mutable storage. However, overuse can lead to global state issues. ### 2.1. Standards for Using Vars * **Do This:** Use vars for configuration values that rarely change or for thread-local storage. * **Don't Do This:** Use vars for application state that is frequently modified or shared between threads unless thread isolation is explicitly managed. ### 2.2. Why This Matters Global mutable state (vars) can make code harder to reason about, especially in concurrent environments. Uncontrolled mutation can lead to race conditions and unpredictable behavior. ### 2.3. Code Examples """clojure (ns my-app.config (:defonce +config+ (atom {}))) ; Use defonce to initialize only once (defn set-config! [key value] (swap! +config+ assoc key value)) (defn get-config [key] (@+config+ key)) ;; Example Usage: (set-config! :database-url "jdbc:...") (println (get-config :database-url)) """ ### 2.4 Anti-patterns * **Over-reliance on Vars:** Using vars for core application logic instead of local bindings and immutable data flow. * **Uncontrolled Mutation:** Modifying vars without consideration for concurrency or data consistency. ## 3. Atoms: Mutable References Atoms provide a mechanism for managing mutable state with atomic updates. ### 3.1. Standards for Using Atoms * **Do This:** Use atoms for managing shared mutable state that requires atomic updates. Use "swap!" and "compare-and-set!" for modifying the atom's value safely. * **Don't Do This:** Perform long-running or potentially blocking operations within "swap!" as this can degrade performance. ### 3.2. Why This Matters Atoms ensure that updates to the state are atomic, preventing race conditions and data corruption in concurrent environments. ### 3.3. Code Examples """clojure (def counter (atom 0)) (defn increment! [] (swap! counter inc)) (defn get-count [] @counter) ;; Example Usage: (future (dotimes [_ 1000] (increment!))) (future (dotimes [_ 1000] (increment!))) (Thread/sleep 100) ; give futures some time to complete (println (get-count)) ; should be close to 2000 """ ### 3.4. Anti-patterns * **Direct Dereferencing without Atomic Updates:** Directly dereferencing the atom ("@atom-name") and then modifying and resetting it, leading to possible race conditions. Always use "swap!" or "compare-and-set!". * **Using Atoms for Immutable Data:** Using atoms to hold immutable data structures needlessly. * **Over-contention:** When multiple threads are constantly trying to update the same atom, it can lead to contention and performance issues. ## 4. Refs and Transactions Refs provide transactional state management, ensuring that a series of operations are performed atomically. ### 4.1. Standards for Using Refs * **Do This:** Use refs for managing shared state where consistency across multiple, related values is crucial. Update refs within "dosync" blocks to ensure transactional semantics. * **Don't Do This:** Perform I/O operations or other side effects within "dosync" blocks as these can violate the atomicity and isolation guarantees. ### 4.2. Why This Matters Refs guarantee ACID (Atomicity, Consistency, Isolation, Durability) properties for state updates, crucial for maintaining data integrity in complex systems. ### 4.3. Code Examples """clojure (def account1 (ref 100)) (def account2 (ref 0)) (defn transfer! [amount] (dosync (if (>= @account1 amount) (do (alter account1 #(- % amount)) (alter account2 #(+ % amount))) (throw (Exception. "Insufficient funds"))))) ;; Example Usage: (try (transfer! 50) (println "Transfer successful") (catch Exception e (println "Transfer failed:" (.getMessage e)))) (println "Account 1:" @account1) ; 50 (println "Account 2:" @account2) ; 50 """ ### 4.4. Anti-patterns * **Performing I/O in Transactions:** Performing potentially failing I/O operations inside "dosync" blocks. * **Long-running Transactions:** Holding transactions open for extended periods, leading to contention and reduced concurrency. ## 5. Agents: Asynchronous State Updates Agents provide a mechanism for managing asynchronous state updates. ### 5.1. Standards for Using Agents * **Do This:** Use agents for performing asynchronous, potentially long-running operations that update state. * **Don't Do This:** Rely on agents for immediate, synchronous updates. Agents are designed for asynchronous processing. ### 5.2. Why This Matters Agents allow decoupling state updates from the main thread, improving responsiveness and preventing blocking. ### 5.3. Code Examples """clojure (def logger (agent [])) (defn log-message [message] (send logger conj message)) ;; Use send to ensure proper agent queueing (defn get-logs [] @logger) ;;Example Usage: (log-message "Starting process...") (log-message "Processing data...") (log-message "Process completed.") (await-for 100 logger) ; Wait for agent to process all messages (println (get-logs)) """ ### 5.4. Anti-patterns * **Synchronous Agent Use:** Using "send-off" when "send" is required, resulting in unintended asynchronous dispatch, or vice versa. * **Ignoring Agent Errors:** Failing to handle exceptions thrown during agent actions; agents halt on exceptions by default. Use "set-error-handler!" on each agent, even if the handler just logs the error. ## 6. Component and Integrant: Managing Application Lifecycle and State Dependencies Component and Integrant are libraries that provide a structured approach to managing application lifecycle and dependencies. ### 6.1. Standards for Using Component/Integrant * **Do This:** Use Component or Integrant for applications with more than a few basic services/dependencies and especially when application shutdown is an important consideration. Define components as records implementing "Lifecycle" protocol (for Component) or use the component keys and init/halt functions as the core of Integrant. * **Don't Do This:** Build complex and difficult-to-test systems manually managing startup/shutdown state. ### 6.2. Why This Matters These libraries offer a declarative way to define application components, their dependencies, and their lifecycle (start/stop). This promotes modularity, testability, and maintainability. Integrant works with plain data and functions making it a great fit for Clojure. ### 6.3. Code Examples (Integrant) """clojure (ns my-app.system (:require [integrant.core :as ig])) (defmethod ig/init-key :db/database [_ config] (println "Initializing database with config:" config) {:connection (atom {})}) ; Replace with actual DB initialization (defmethod ig/halt-key :db/database [_ db] (println "Closing database connection") ;; Close DB connection here ) (defmethod ig/init-key :web/server [_ {:keys [db port]}] (println "Starting web server on port" port) {:server (atom {:db db :port port})}) ; Simulate a web server (defmethod ig/halt-key :web/server [_ server] (println "Stopping web server") ;; Stop web server here ) (def system-config {:db/database {:url "jdbc:..."} :web/server {:db (ig/ref :db/database) :port 8080}}) ;; Example usage: (def system (ig/init system-config)) ;; ... application runs ... (ig/halt! system) """ ### 6.4. Anti-patterns * **Ignoring Component Lifecycle:** Failing to properly start and stop components, leading to resource leaks or unexpected behavior. * **Tight Coupling:** Creating tight dependencies between components, reducing modularity and testability. ## 7. Managing Reactivity with "re-frame" and Fulcro For UI-intensive applications, libraries like "re-frame" and Fulcro provide a structured way to manage application state and reactivity. ### 7.1. Standards for Using "re-frame" and Fulcro * **Do This:** Adopt "re-frame"'s event-driven architecture or Fulcro's data-driven (normalized) approach for managing application state in complex UIs. Use subscriptions for deriving UI state from the central data store. * **Don't Do This:** Mutate application state directly within UI components, bypassing the "re-frame" event handling or Fulcro mutations. ### 7.2. Why This Matters These libraries provide a predictable and efficient mechanism for updating the UI in response to application state changes. ### 7.3. Code Example ("re-frame") """clojure (ns my-app.re-frame (:require [re-frame.core :as rf])) ;;Define an event handler (rf/reg-event-fx :increment (fn [{:keys [db]} [_]] {:db (update db :counter inc)})) ;; Define a subscription (rf/reg-sub :counter (fn [db [_]] (:counter db))) ;; Usage in a component (Om Next example) (defn my-component [] (let [counter (rf/subscribe [:counter])] (fn [] (dom/div (dom/h1 (str "Counter: " @counter)) (dom/button {:on-click #(rf/dispatch [:increment])} "Increment"))))) """ ### 7.4. Anti-patterns * **Direct State Mutation:** Modifying the "re-frame" or Fulcro application database directly instead of using events or mutations. * **Complex Subscriptions:** Creating overly complex subscriptions that perform heavy computations, impacting UI performance. ## 8. Metadata and Watches Clojure provides a mechanism for attaching metadata to data structures and setting up watches to observe changes to vars, atoms, refs, and agents. ### 8.1. Standards for metadata and watches * **Do This:** Use metadata to attach auxiliary information to data structures without modifying their core functionality. Use watches to observe changes in state variables during development and debugging or react to state changes in production. * **Don't Do This:** Overuse watches in production as they can negatively affect performance. ### 8.2. Why This Matters Metadata allows associating additional information with data without altering its structure or semantics. Watches provides notifications when the value of a stateful value changes. ### 8.3. Code Examples """clojure ;; Using metadata (def my-vector (with-meta [1 2 3] {:description "A simple vector"})) (println (meta my-vector)) ;; Prints: {:description "A simple vector"} ;; Using watches (def my-atom (atom 0)) (add-watch my-atom :my-watch (fn [key atom old-state new-state] (println "Atom" key "changed from" old-state "to" new-state))) (swap! my-atom inc) ;; Output: Atom :my-watch changed from 0 to 1 (remove-watch my-atom :my-watch) """ ### 8.4. Anti-patterns * **Over-reliance on Watches for Core Logic:** Do not implement business logic inside of watch functions as they are for debugging. * **Ignoring Watch Performance:** Using watches in performance-critical sections of code without considering their impact. * **Abusing Metadata:** Attaching critical application data or logic to metadata, making it less discoverable and harder to maintain. ## 9. General Recommendations * **Choose the Right Tool:** Select the appropriate state management mechanism based on the specific requirements of your application. * **Keep State Local:** Minimize the scope of mutable state to reduce complexity and improve testability. * **Embrace Immutability:** Favor immutable data structures whenever possible to simplify reasoning about code and prevent side effects. * **Test Thoroughly:** Write comprehensive tests to ensure that state transitions are correct and that concurrent access is handled safely. * **Monitor Performance:** Use performance monitoring tools to identify and address any bottlenecks related to state management. * **Avoid Legacy Code Patterns:** Clojure evolves, avoid techniques that are out of date or have known drawbacks. * **Always document the reason behind state selection**: Explain briefly why a specific approach for managing state has been selected in a particular context. By following these standards, you can create Clojure applications that are maintainable, performant, and robust. Remember that this document should be adapted according to particular use cases and team conventions.