the transaction should not try to take money from an account until there is enough money in it: transfer :: Integer -> Account -> Account -> STM () transfer amount from to = do fromVal <- readTVar from if (fromVal - amount) >= 0 then do debit amount from credit amount to else retry Here the retry function has been used, which will roll back a transaction, and try it again. Retrying in STM is smart in that it won't try to run the transaction again until one of the variables it references during the transaction has been modified by some other transactional code. This makes the STM monad quite efficient. An example program using the transfer function might look like this: module Main where import Control.Concurrent (forkIO) import Control.Concurrent.STM import Control.Monad (forever) import System.Exit (exitSuccess) type Account = TVar Integer main = do bob <- newAccount 10000 jill <- newAccount 4000 repeatIO 2000 $ forkIO $ atomically $ transfer 1 bob jill forever $ do bobBalance <- atomically $ readTVar bob jillBalance <- atomically $ readTVar jill putStrLn ("Bob's balance: " ++ show bobBalance ++ ", Jill's balance: " ++ show jillBalance) if bobBalance == 8000 then exitSuccess else putStrLn "Trying again." repeatIO :: Integer -> IO a -> IO a repeatIO 1 m = m repeatIO n m = m >> repeatIO (n - 1) m newAccount :: Integer -> IO Account newAccount amount = newTVarIO amount transfer :: Integer -> Account -> Account -> STM () transfer amount from to = do fromVal <- readTVar from if (fromVal - amount) >= 0 then do debit amount from credit amount to else retry credit :: Integer -> Account -> STM () credit amount account = do current <- readTVar account writeTVar account (current + amount) debit :: Integer -> Account -> STM () debit amount account = do current <- readTVar account writeTVar account (current - amount) which should print out "Bob's balance: 8000, Jill's balance: 6000". Here the atomically function has been used to run STM actions in the IO monad. References Haskell programming language family