I love how this is done in Software Transactional Memory (STM) in Haskell. There, the transaction code happens in its own type (monad), and there is an explicit conversion function called `atomically :: STM a -> IO a`, which carries out the transaction.
This means that the transaction becomes its own block, clearly separated, but which can reference pure values in the surrounding context.
do
…
some IO stuff
…
res <- atomically $ do
…
transaction code
which can reference
results from the IO above
…
…
More IO code using res, which is the result of the transaction
…