Background
I am studying the Transaction Script pattern, namely from a book called “Learning Domain Driven Design”.
For those of you unfamiliar with this pattern, here is a small description:
The Transaction Script organizes business logic by procedures, where each procedure handles a single request from the Public Interface of the application, aka, the presentation layer.
In the book, the author explains that one of the limitations of this pattern is when you have to take an action that must be atomic across several different storage/communication mechanisms. Imagine updating the database and sending a message via a broker - for the system to remain consistent, both must be done atomically.
@spec execute_v1(integer(), NaiveDateTime.t()) :: :ok | {:error, any()}
def execute_v1(user_id, visited_on) do
Repo.query!("UPDATE \"user\" SET last_visit=$1 WHERE id=$2", [visited_on, user_id])
MessageBus.publish(%{user_id: user_id, visited_on: visited_on})
end
Question
In most languages, ensuring this behaviour remains consistent and transactional would be impossible to do. In fact this is the main premise to introduce CQRS and the Outbox Pattern in later chapters.
However Elixir does have a workaround for this limitation, that can make sure this set of operations is atomic and consequently ensures the consistency of the system (or so I believe):
@spec execute_v2(integer(), NaiveDateTime.t()) :: {:ok, [Postgrex.Result.t()]}
def execute_v2(user_id, visited_on) do
Repo.transact(fn ->
update_result =
Repo.query!("UPDATE \"user\" SET last_visit=$1 WHERE id=$2", [visited_on, user_id])
:ok = MessageBus.publish(%{user_id: user_id, visited_on: visited_on})
{:ok, [update_result]}
end)
end
By using Repo.transact Elixir allows us to make sure this piece of code is atomic. If publishing of the message fails, the Database operation is rolled back. In this specific instance, because we only send 1 message, I believe this workaround would work just fine.
Even though I am using this workaround in this context, I would like to make it clear I have seen people use it with all sorts of things. A few come to my mind:
- Sending HTTP messages after writing to the database
- Doing database writes on multiple storage systems (usually relational DBs and non-relational DBs at the same time)
However, I always have the same questions.
- Is
Repo.transactsupposed to be used this way? - Isn’t this considered an anti-pattern by the community?
- What are the alternatives to this workaround, if any?
I am also curious to know if anyone else reading the code thinks this can fail in a way I have not yet predicted. Please let me know!






















