Sometimes in my system, I want to change something in the database and also do some other side-effect, for example, filling a cache or sending a message, etc.
Normally what I do is something like this:
alias Ecto.Multi Multi.new() |> Multi.insert(:insert_data_to_db, data) |> Multi.run(:update_cache, fn _, _ -> :ok = MyCache.update(data) end) |> Repo.transaction(timeout: :infinity)
This works great since if my cache update fails for some reason, my insertion will fail too, avoiding situations that I would have successfully inserted my data into the database and failed to update the cache leaving it into an inconsistent state.
Now, let’s say that I want to also send the data to another process that needs to be done atomically too (it can be another thing too, the process is just an example, but it can be anything that needs to be added atomically).
alias Ecto.Multi Multi.new() |> Multi.insert(:insert_data_to_db, data) |> Multi.run(:update_cache, fn _, _ -> :ok = MyCache.update(data) end) |> Multi.run(:send_data, fn _, _ -> :ok = SendData.send(data) end) |> Repo.transaction(timeout: :infinity)
Now the approach above doesn’t work anymore since if send data fails, the database will rollback, but the cache will not.
My question is, how do you guys handle this type of situation that you need to do multiple things and it all needs to be in a “apply all or apply nothing if something fails” manner?
Also, this example I used Ecto, but it can be another database too, or even without a database at all.
Another example, I have one situation that I store the last ids of some data I receive in my node, and I also need to send this data to clients via notification. So I first send the notifications then add they ids to the mnesia database, but if the notifications are sent correctly but my mnesia write fails, then again I would have an inconsistent state.
These types of situations seem very common to me, so I’m really wondering if there is a better solution and how you guys handle it.