Consider your chain of examples for "Upload something to S3, if X then call some web service, otherwise log and call something else, etc etc etc." You could represent this as a sequence of steps which could be interpreted by a central side-effecting loop. Let's call it an "action plan", in this case for your "upload" operation:
(defn upload-plan [req]
[{:action :upload
:file (get-in req [:params :doc])
:max-size 2000
:name :doc}
{:action :notify
:message (fn [state] (get-in state [:data :doc]))
:target [:somebody]}])
Now you can process this plan centrally: (defmulti take-action (fn [state {:keys [action]}] action))
(defmethod take-action :upload ...)
(defmethod take-action :notify ...)
(defn execute [plan]
(let [{:keys [status]} (reduce take-action {:status :ok, :data {}} plan)]
(when (not= status :ok)
(log "omg!"))))
While this is initially a tad more verbose, there's great reasons to do this sort of thing.For example, now have a central place to mock all side effects! You could substitute a different execute method during testing, or automatically log the "action" object that failed.
More importantly, you can develop each of your "plan" functions independently. You can reuse them, re-order them, etc.
You can also do hypotheticals, for example: "Which plan would cause less network round trips?" can be answered before committing to a plan.
You can _record_ plans. Instrument your execute method to record and save plans to a file, then you can just check them right in as test cases.
You can optimize plans. If five different functions all notify the same target of the same thing, your central execution method can filter subsequent calls. Or you could separate your "optimization" from your "interpretation" and you can ask "After accounting for idempotence, which operations will be performed?"
The list goes on and on...