Skip to main content
Version: 2.0.x

STM

An STM[E, A] represents an effect that can be performed transactionally resulting in a failure E or a success A. There is a more powerful variant ZSTM[R, E, A] which supports an environment type R like ZIO[R, E, A].

The STM (and ZSTM variant) data-type is not as powerful as the ZIO[R, E, A] datatype as it does not allow you to perform arbitrary effects. These are because actions inside STM actions can be executed an arbitrary amount of times (and rolled-back as well). Only STM actions and pure computation may be performed inside a memory transaction.

No STM actions can be performed outside a transaction, so you cannot accidentally read or write a transactional data structure outside the protection of STM.atomically (or without explicitly committing the transaction). For example:

import zio._
import zio.stm._

def transferMoney(from: TRef[Long], to: TRef[Long], amount: Long): STM[String, Long] =
for {
senderBal <- from.get
_ <- if (senderBal < amount) STM.fail("Not enough money")
else STM.unit
_ <- from.update(existing => existing - amount)
_ <- to.update(existing => existing + amount)
recvBal <- to.get
} yield recvBal

val program: IO[String, Long] = for {
sndAcc <- STM.atomically(TRef.make(1000L))
rcvAcc <- STM.atomically(TRef.make(0L))
recvAmt <- STM.atomically(transferMoney(sndAcc, rcvAcc, 500L))
} yield recvAmt

transferMoney describes an atomic transfer process between a sender and a receiver. The transaction will fail if the sender does not have enough of money in their account. This means that individual accounts will be debited and credited atomically. If the transaction fails in the middle, the entire process will be rolled back, and it will appear that nothing has happened.

Here, we see that STM effects compose using a for-comprehension and that wrapping an STM effect with STM.atomically (or calling commit on any STM effect) turns the STM effect into a ZIO effect which can be executed.

STM transactions compose sequentially. By using STM.atomically (or commit), the programmer identifies atomic transaction in the sense that the entire set of operations within STM.atomically appears to take place indivisibly.

Errors

STM supports errors just like ZIO via the error channel. In transferMoney, we saw an example of an error (STM.fail).

Errors in STM have abort semantics: if an atomic transaction encounters an error, the transaction is rolled back with no effect.

retry

STM.retry is central to making transactions composable when they may block. For example, if we wanted to ensure that the money transfer took place when the sender had enough of money (instead of failing right away), we can use STM.retry instead:

def transferMoneyNoMatterWhat(from: TRef[Long], to: TRef[Long], amount: Long): STM[String, Long] =
for {
senderBal <- from.get
_ <- if (senderBal < amount) STM.retry else STM.unit
_ <- from.update(existing => existing - amount)
_ <- to.update(existing => existing + amount)
recvBal <- to.get
} yield recvBal

STM.retry will abort and retry the entire transaction until it succeeds (instead of failing like the previous example).

Note that the transaction will only be retried when one of the underlying transactional data structures have been changed.

There are many other variants of the STM.retry combinator like STM.check so rather than writing if (senderBal < amount) STM.retry else STM.unit, you can replace it with STM.check(senderBal < amount).

Composing alternatives

STM transactions compose sequentially so that both STM effects are executed. However, STM transactions can also compose transactions as alternatives so that only one STM effect is executed by making use of orTry on STM effects.

Provided we have two STM effects sA and sB, you can express that you would like to compose the two using sA orTry sB. The transaction would first attempt to run sA and if it retries then sA is abandoned with no effect and then sB runs. Now if sB also retries then the entire call retries. However, it waits for the transactional data structures to change that are involved in either sA or sB.

Using orTry is an elegant technique that can be used to determine whether or not an STM transaction needs to block. For example, we can take transferMoneyNoMatterWhat and turn it into an STM transaction that will fail immediately if the sender does not have enough of money instead of retrying by doing:

def transferMoneyFailFast(from: TRef[Long], to: TRef[Long], amount: Long): STM[String, Long] =
transferMoneyNoMatterWhat(from, to, amount) orTry STM.fail("Sender does not have enough of money")

This will cause the transfer to fail immediately if the sender does not have money because of the semantics of orTry.