Failsage is a Clojure library that provides an idiomatic translation layer for Failsafe.dev, making it easier to use Failsafe's battle-tested resilience patterns from Clojure.
Failsage is designed for simplicity and ergonomics. You can pass policies directly to execute functions without creating executors, use optional context bindings only when you need execution state, and compose multiple policies through simple vectors. Rather than hiding context in dynamic variables or nesting macros, failsage makes state explicit and easy to test. It provides first-class async integration with futurama using the same straightforward API. The result is a library where simple cases require minimal code and complex scenarios remain clear and composable.
Note: You can also use Failsafe directly via Java interop or through alternatives like diehard.
Rather than reinventing the resilience patterns, Failsage wisely wraps Failsafe with:
Failsage supports all core Failsafe policies:
For detailed information on each policy's behavior and configuration options, see the Failsafe documentation.
(require '[failsage.core :as fs])
;; Simplest form - no executor needed
(fs/execute
(call-unreliable-service))
;; With a retry policy
(def retry-policy
(fs/retry {:max-retries 3
:delay-ms 100
:backoff-delay-factor 2.0}))
;; Pass policy directly - no need to create executor
(fs/execute retry-policy
(call-unreliable-service))
;; Or create an executor explicitly
(def executor (fs/executor retry-policy))
(fs/execute executor
(call-unreliable-service))
(def retry-policy
(fs/retry {:max-retries 3
:backoff-delay-ms 100
:backoff-max-delay-ms 5000
:backoff-delay-factor 2.0}))
;; Pass policy directly
(fs/execute retry-policy
(http/get "https://api.example.com/data"))
(def circuit-breaker
(fs/circuit-breaker {:delay-ms 60000 ;; Wait 1 minute before half-open
:failure-threshold 5 ;; Open after 5 consecutive failures
:on-open-fn (fn [e] (log/warn "Circuit breaker opened!"))
:on-close-fn (fn [e] (log/info "Circuit breaker closed!"))}))
;; Pass policy directly
(fs/execute circuit-breaker
(call-flaky-service))
(def fallback-policy
(fs/fallback {:result {:status :degraded :data []}
:handle-exception Exception}))
;; Returns fallback value on any exception
(fs/execute fallback-policy
(fetch-user-data user-id))
;; => {:status :degraded :data []}
(def timeout-policy
(fs/timeout {:timeout-ms 5000 ;; 5 second timeout
:interrupt true})) ;; Interrupt thread on timeout
;; Pass policy directly
(fs/execute timeout-policy
(slow-database-query))
;; Allow 100 requests per second
(def rate-limiter
(fs/rate-limiter {:max-executions 100
:period-ms 1000
:burst true}))
;; Pass policy directly
(fs/execute rate-limiter
(process-request request))
;; Limit to 10 concurrent executions
(def bulkhead-policy
(fs/bulkhead {:max-concurrency 10
:max-wait-time-ms 1000})) ;; Wait up to 1 second for permit
;; Pass policy directly
(fs/execute bulkhead-policy
(process-task task))
Policies can be composed together. They are applied in order from innermost to outermost:
;; Compose retry, circuit breaker, and fallback
;; Execution order: retry -> circuit breaker -> fallback
(def retry-policy (fs/retry {:max-retries 3}))
(def cb-policy (fs/circuit-breaker {:failure-threshold 5 :delay-ms 60000}))
(def fallback-policy (fs/fallback {:result :fallback-value}))
;; Policies compose: fallback wraps circuit-breaker wraps retry
(def executor (fs/executor [fallback-policy cb-policy retry-policy]))
(fs/execute executor
(unreliable-operation))
;; - First tries the operation
;; - Retries up to 3 times on failure
;; - Circuit breaker tracks failures
;; - If everything fails, fallback returns :fallback-value
Timeout + Retry: Prevent long waits while retrying
(def timeout-policy (fs/timeout {:timeout-ms 2000}))
(def retry-policy (fs/retry {:max-retries 3 :delay-ms 100}))
;; Timeout applies to EACH retry attempt
(def executor (fs/executor [retry-policy timeout-policy]))
Bulkhead + Circuit Breaker + Fallback: Complete resilience stack
(def bulkhead (fs/bulkhead {:max-concurrency 20}))
(def cb (fs/circuit-breaker {:failure-threshold 10 :delay-ms 30000}))
(def fallback (fs/fallback {:result {:status :degraded}}))
;; Limit concurrency, break on failures, degrade gracefully
(def executor (fs/executor [fallback cb bulkhead]))
Failsage integrates with futurama for async execution:
(require '[failsage.core :as fs])
(require '[futurama.core :as f])
;; Simplest form - uses default thread pool
(fs/execute-async
(f/async
(let [result (f/<!< (async-http-call))]
(process-result result))))
;; With a policy
(def retry-policy (fs/retry {:max-retries 3}))
(fs/execute-async retry-policy
(f/async
(let [result (f/<!< (async-http-call))]
(process-result result))))
;; With explicit executor and thread pool
(def executor (fs/executor :io retry-policy)) ;; Use :io thread pool
(fs/execute-async executor
(f/async
(let [result (f/<!< (async-http-call))]
(process-result result))))
You can access execution context information (like attempt count, start time, etc.) by providing a context binding:
;; Synchronous execution with context
(def retry-policy (fs/retry {:max-retries 3}))
(fs/execute retry-policy ctx
(let [attempt (.getAttemptCount ctx)
start-time (.getStartTime ctx)]
(log/info "Attempt" attempt "started at" start-time)
(call-service)))
;; Asynchronous execution with context
(fs/execute-async retry-policy ctx
(f/async
(log/info "Async attempt" (.getAttemptCount ctx))
(f/<!< (async-call-service))))
The context object provides access to:
.getAttemptCount
- Current attempt number (0-indexed).getStartTime
- When execution started.getElapsedTime
- Time elapsed since execution startedAll policies support event callbacks for observability:
(def retry-policy
(fs/retry {:max-retries 3
:on-retry-fn (fn [event]
(log/info "Retrying after failure"
{:attempt (.getAttemptCount event)
:exception (.getLastException event)}))
:on-success-fn (fn [event]
(log/debug "Execution succeeded"))
:on-failure-fn (fn [event]
(log/error "Execution failed after retries"))}))
Handle specific exceptions or results:
;; Retry only on specific exceptions
(def retry-policy
(fs/retry {:max-retries 3
:handle-exception [java.net.SocketTimeoutException
java.io.IOException]}))
;; Retry based on result
(def retry-policy
(fs/retry {:max-retries 5
:handle-result-fn (fn [result]
(and (map? result)
(= (:status result) :retry)))}))
;; Abort retry on specific exceptions
(def retry-policy
(fs/retry {:max-retries 10
:abort-on-exception [IllegalArgumentException
SecurityException]}))
See the test suite for comprehensive examples of all policies and configuration options.
For detailed documentation on policy behavior, configuration, and advanced patterns, refer to the Failsafe documentation.
failsage is built, tested, and deployed using Clojure Tools Deps.
GNU Make is used to simplify invocation of some commands.
failsage releases for this project are on Clojars. Simply add the following to your project:
See CONTRIBUTING.md
Copyright 2025 Jose Gomez
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
Ctrl+k | Jump to recent docs |
← | Move to previous article |
→ | Move to next article |
Ctrl+/ | Jump to the search field |