Skip to main content
Clojure, despite being a well-designed functional language, has several common anti-patterns that can lead to performance issues, maintainability problems, and bugs. Here are the most important anti-patterns to avoid when writing Clojure code.
Avoid using mutable state (atoms, refs, agents) when pure functions would suffice. Embrace Clojure’s functional programming paradigm and use immutable data structures by default.
Use threading macros (->, ->>, as->, etc.) to make code more readable by avoiding deeply nested function calls. Threading macros make the data flow explicit and easier to follow.
Prefer Clojure’s persistent collections over Java’s mutable collections. Clojure’s collections are immutable, thread-safe, and work seamlessly with the rest of the language.
Prefer higher-order functions like map, filter, reduce, etc., over manual recursion with loop/recur. Higher-order functions are more declarative and often more readable.
Use destructuring to extract values from data structures. It makes code more concise and expressive, especially when working with nested data structures.
Avoid reflection by using type hints when working with Java interop or performance-critical code. Reflection is slow and can cause unexpected performance issues.
Use transducers when composing multiple transformations on a collection. Transducers avoid creating intermediate collections and can be more efficient.
Avoid using dynamic vars (^:dynamic) for configuration or state that could be passed explicitly. Dynamic vars make code harder to reason about and test.
Use proper exception handling with try/catch/finally for operations that might fail, especially I/O operations. Consider using libraries like ex-info and ex-data for structured error handling.
Use clojure.spec for data validation and documentation. Specs provide a declarative way to define the shape and constraints of your data.
While using keywords as functions (e.g., :name user) is idiomatic in Clojure, excessive use can make code less readable. Consider using destructuring for clarity.
Organize your code into logical namespaces with clear responsibilities. Follow the principle of cohesion: each namespace should have a single, well-defined purpose.
Be careful with nil punning (relying on nil to propagate through a chain of operations). While it can be convenient, it can also hide bugs and make code harder to debug.
Write proper tests using clojure.test or other testing libraries. Tests help ensure your code works as expected and catch regressions.
Document your functions and namespaces with docstrings. Include descriptions of arguments, return values, and usage examples.
Use a component lifecycle management library like Component, Mount, or Integrant for managing stateful resources. These libraries help with initialization, shutdown, and dependency injection.
Provide descriptive error messages and use ex-info to include structured data with exceptions. This makes debugging easier and allows for programmatic handling of specific error conditions.
Avoid using def inside functions to create or modify global variables. This leads to side effects and makes code harder to reason about and test.
Use dependency injection or a component system to manage dependencies between parts of your application. This makes your code more modular, testable, and flexible.
Use the appropriate concurrency primitives for your use case. Atoms are good for independent values, refs for coordinated changes, agents for asynchronous updates, and Java’s concurrency utilities for more complex coordination.
Avoid premature optimization. Start with clear, simple code, then profile to identify actual bottlenecks. Use tools like criterium for benchmarking and YourKit or VisualVM for profiling.