Idempotency with side-effects
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Failed to load latest commit information.


Timed idempotency with side-effects

Benjamin gives you a macro that transforms code like this:

(let [logbook (get-logbook entity)]
  (when (some pred logbook)
    (let [response (body)]
      (when (success? response)
        (write logbook)))))

Into this:

(with-logbook entity :event

There is a blog post that delves in the motivation and backstory.



In your namespace, require:

[benjamin.core :refer [with-logbook]]


  • logbook-fn A function that retrieves a logbook given an entity.
  • persistence-fn A function that persists an updated logbook given an entity and an event
  • success-fn A predicate function that determines the succes of body.
  • events A Clojure map with events as keys and predicates as values.
  • allow-undeclared-events? a boolean that allows of forbids events that are not registered in the events map.

Tip: system users can configure this library via a component that ships with the latest snapshot.

Manual configuration is done by requiring:

[benjamin.configuration :refer [set-config!]]

Accessing the logbook

(set-config! :logbook-fn f)

logbook-fn is a function that receives the entity as argument and returns a logbook. The default is :logbook which will work when the entity map embeds the logbook, as in:

{:first-name "Benjamin"
 :last-name "Peirce"
 :occupation "Mathematician"
 :email ""
 :logbook [{:welcome-email timestamp}
           {:subscription-reminder timestamp}
           {:subscription-reminder timestamp}
           {:newsletter timestamp}
           {:newsletter timestamp}
           {:newsletter timestamp}]}

Deriving predicates from events

(set-config! :events {:event predicate
                      :event predicate
                      :event predicate

Predicates are one argument functions that receive a logbook entry. A logbook entry is a map with an event as the key and a timestamp as the value.

The following example checks if the logbook entry was written today.

#(if-let [date (first (vals %))]
   (time/today? date)

Several predicates are offered in the benjamin.predicates namespace for convenience. That namespace has a dependency that you need to require in your build should you want to use them. This is because benjamin does not have any dependency of its own (it relies entirely on language features).

Writing to the logbook

:persistence-fn is a function of two arguments, entity and event. Its responsibility is to append to the logbook and persist the entity. You have to provide an implementation or an error will be thrown. For example:

(set-config! :persistence-fn
             (fn [entity event] (let [logbook (conj (:logbook entity) {event (t/now)})]
                                 (assoc entity :logbook logbook)
                                 (save db entity))))

Tip: If you have dependencies (as a reference to the db), use a higher–order function that returns persistence-fn.

(defn logbook [{{store :store} :db :as dependencies}]
  (fn [entity event] (let [logbook (conj (:logbook entity) {event (t/now)})]
                       (assoc entity :logbook logbook)
                       (save db entity)))

Tip: The benjamin component in the system library includes an option that will wrap dependencies associated with it in the system map.

Determining the status of the side-effect

The success function is a function of one argument, ie. the return value of the side-effectful body. It determines if the operation was successful and thus for inclusion in the logbook.

(set-config! :success-fn (constantly true))

The default assumes all your operations will be A-okay. You’ll probably want to pass along something more realistic.

Strict or lax policy with unknown events

(with-logbook entity event

If the event is unkown, that is if it doesn’t show up in the events map, no predicate can be derived and then we rely on a policy you can set yourself. Either we accept unknown events and we proceed with the side-effect, or we reject them and return immediately. The default is strict, but you can change that.

(set-config! :allow-undeclared-events? true)


A test suite is provided in benjamin.core-test. Call (test-ns *ns*) in the namespace, or run boot testing for continous testing.


You can work with as many entities you want. You can declare as many events as you want. You can have any side-effectful procedures in the body. Your success-fn may dispatch on the return value if you run different types of operations in the body.

The configuration is a singleton with dynamic scope, so deal with it to the best of your understanding. Personally, I set it once and treat it as a constant for the lifetime of the application.


Licensing terms will be revealed shortly. In the meantime, do what you want with it.