Suppose you have some bank accounts:
(def accounts
[(ref 0)
(ref 10)
(ref 20)
(ref 30)])
And a atomic "transfer" function:
(defn transfer [src-account dest-account amount]
(dosync
(alter dest-account + amount)
(alter src-account - amount)))
Which works as follows:
(transfer (accounts 1) (accounts 0) 5)
(map deref accounts)
=> (5 5 20 30)
You can then easily compose the transfer function to create a higher level transaction, for example transferring from multiple accounts:
(defn transfer-from-all [src-accounts dest-account amount]
(dosync
(doseq [src src-accounts]
(transfer src dest-account amount))))
(transfer-from-all
[(accounts 0) (accounts 1) (accounts 2)]
(accounts 3)
5)
(map deref accounts)
=> (0 0 15 45)
Note that all of the multiple transfers happened in a single, combined transaction, i.e. it was possible to "compose" the smaller transactions.
To do this with locks would get complicated very quickly: assuming the accounts needed to be individually locked then you'd need to do something like establishing a protocol on lock acquisition order in order to avoid deadlocks. It's very easy to make a hard-to-detect mistake. STM saves you from all this pain.