If I have a User and I need to transact the “balance” when say I transfer money between two users, but the rest of the User data is transactionaly independent, like updating the email or phone number.
Since I want a mutable User, I’ll use an Atom to store the User state, and I could use a Ref to store the balance inside the User atom:
(def user
(atom
{:email "user@user.com"
:phone [111 222 3333]
:balance (ref 200)}))
You can see an atom won’t work here, because we need to transact between two users, and you can’t do a swap
over two atoms together, that’s why a “ref” is needed here.
We could wrap the user in a ref instead of an atom, that would work as well, but the “email” and “phone” don’t need transactions across users, only within the same user, so a “ref” isn’t needed for those and I think would be slower, though I’m not sure.
Now I also need to store all my users, and I want to be able to add/remove users. So I’m going to have a map of username → User instead of a single user:
(def users
(atom
{:user1
(atom
{:email "user@user.com"
:phone [111 222 3333]
:balance (ref 200)})}))
But now why keep all these inner atoms and inner refs? It even gets confusing if say you’re modifying the outer atom in one thread and the ref in another, what happens?
So you’ll often just remove them:
(def users
(atom
{:user1
{:email "user@user.com"
:phone [111 222 3333]
:balance 200}}))
The ref isn’t needed anymore because the balance transaction would have been between two users, and now that’s a single atomic swap which handles the transaction.
In my experience this is the kind of scenario where it’s why I don’t end up using refs.
Even if you took this further, say we have Business which also has “balance” that can transfer money back and forth to Users. We need transactional support again to make sure we added and removed successfully to the User and from the Business.
(def users
(atom
{:user1
{:email "user@user.com"
:phone [111 222 3333]
:balance 200}}))
(def businesses
(atom
{:business1
{:balance 3450}}))
Here we need a solution for the transaction accross User balance and Business balance again. We could make the “balance” a ref again, or we could make the “users” and “businesses” maps a ref instead of an atom, etc.
But here too, I think quickly you figure out why not have a state map:
(def datastore
(atom
{:users
{:user1
{:email "user@user.com"
:phone [111 222 3333]
:balance 200}}
:businesses
{:business1
{:balance 3450}}}))
Now I think there could be a performance argument. If you’re modifying this atom a lot, there’ll be a lot of contention, so maybe a design as one of the ones we had before, where the state was broken down into more vars or had some inner atoms or refs could improve performance. That said it gets tricky, refs incur more performance cost than atoms for example, so it’s hard to say which will perform best for your usage patterns.