Transactions are a key feature of relational databases like Postgres. Transactions allow you to combine separate queries into something that is still an atomic unit: it all succeeds or fails together.

As we build up a library of queries in our application, we often want to ensure that the combined queries are always run in a transaction. Let’s say we have the following Python code:

And we want to create a new user, and add one of their food preferences, on a transaction:

We now have this re-usable function, create_user_with_likes, that is always guaranteed to be run in a transaction. We can call it like this:

Behind the scenes, this generates something like the following, to Postgres:

This is all well and good.

Now let’s say we want to combine this function, create_user_with_likes, with another, and run that inside a transaction:

Running this, we generate the following statements to Postgres:

Huh, that’s strange. There are two sets of BEGIN and COMMIT statements. Well, we do call with_transaction twice, one nested inside another. Sadly, this is a problem. As this is what happens:

So, we’ve managed to actually create two separate transactions, when our intention was to make sure we’re in one big transaction.

The choice we have, it seems, is to either not call with_transaction in the definition of create_user_with_likes, or, to say it can’t be combined with other functions to form a larger transaction. Neither of these is particularly satisfying:

If we don’t wrap each query component in a transaction, then the outer-most caller (how do we even know we’re the outer-most caller?) needs to know to wrap it in a transaction.

If we do wrap it in a transaction, then it can’t be combined with other transactions, or, worse, it is, in a way that actually creates multiple transactions.

In Haskell, however, we have another choice. Using types, we can encode a transaction, which, when executed, will run inside a pair of exactly one BEGIN and END statement.

Transliterating the Python to Haskell, we get types like the following:

And with this encoding, we have the same problem. But it turns out we can make a simple change, and instead of doing things directly in IO, we create a new Transaction type, which also allows us to elide the connection parameter. An example type becomes:

Since Transaction implements the Monad interface, we can combine calls together, just like we did before in IO. Now, when we want to actually execute this against the database, we call the only function that allows us to turn this Transaction into an actual side-effecting IO action:

Now, we can combine as many transactions together as we want, all while staying in the Transaction type. And once we want to actually execute this, we call transact. The types ensure that a Transaction is wrapped in exactly one pair of BEGIN and COMMIT.

Something further comes out of this, which is that we can not do long-running IO calls in the middle of a transaction — a common performance problem. The code will not even compile. (If we want to let ourselves do this, we can change the types a bit, it’s an explicit choice we can choose whether or not to make).

We have implemented this technique in an open-source (MIT Licensed) Haskell library: postgresql-transactional. Also available on Github.