Hey everyone ,
we (me and my team) are currently struggling with the idea of reducing the repetition of the same strings in several places within our application. I guess in other languages we would rely on enums.
tl;dr
In the end we introduced a macro to include module attributes with the strings we needed to match on. But aren’t fully convinced, if this is the right approach. Therefore we would like to hear what you all think about this approach
First I will try to give a context and then try to show some examples of the problem, which gave us the feeling that we would want to introduce a “single point of truth”. At the end I show you “the solution” Here we go…
Context & Problem
Our backend is dealing with payment data it receives from an app and handles the communication with an external payment service.
As some requests take a bit longer and we wanted to deal with bad reception, the basic architecture is asynchronous and relies on Oban.
So basically: When the App makes a payment request we create an %PaymentProcess{status: "initialized", payment_provider: "paypal", ....}
, put it in the database, enque an Oban job to deal with the external service interaction and return a 200. The app is then listening to state changes of the payment.
Based on the provider the job handles the interaction with the external service differently and in the end depending on the result it will update the PaymentProcess
like this:
%PaymentProcess{status: "paid", payment_provider: "paypal", ....}
or%PaymentProcess{status: "declined", payment_provider: "paypal", error_code: 123}
After that, it sends the app a notification to fetch the new state of the payment process (the app also pulls on a regular base as a fallback).
When the app now fetches the current PaymentProcess
, we fetch it from the DB and return a 200 if everything went smoothly (status: "paid"
) or 422 when the payment was declined. Part of the error response is also a text the app should display, which is specific to the used payment_provider
. In some cases it is also a mix of the used payment_provider
, the status
and a error_code
.
(We decided to let the backend provide the error text, as it is way easier to change texts, add new cases etc. with a new backend deployment, instead of releasing an app update through the app stores)
Examples of repetition
This flow and setup led to code where we need to pattern match on the payment_provider
in several places.
E.g. does Request
look like this:
defp payment_authorization(%{
payment_authorization_provider: "paypal",
payment_authorization_attributes: %{"fetching" => some, "provider" => specific_data}
}) do
...
end
defp payment_authorization(%{
payment_authorization_provider: "google_pay",
payment_authorization_attributes: %{"other" => specifc_attributes}
}) do
...
end
And the part where we build a failure (lets call it AppFailure
), we do something like this:
defp authorization_failed(payment_provider) do
case payment_provider do
"paypal" ->
{:unprocessable_entity,
"Please do this and that to fix the problem."}
"google_pay" ->
{:unprocessable_entity,
"Open the Google Pay App to update your card details."}
end
end
Especially in this module the repetition in the case statements matching over and over again on "paypal"
,"google_pay"
etc. got pretty obvious.
The first idea was using module attributes at the top:
@paypal "paypal"
@google_pay "google_pay"
...
This helped to reduce repetition and error-proneness, while providing auto-completion with VSCode.
But we still have the same strings in our Request
module and basically wanted a way to share the module attributes. The first idea (we already knew wouldn’t work) looked like this:
defmodule PaymentProviders do
@paypal "paypal"
@google_pay "google_pay"
def paypal, do: @paypal
def google_pay, do: @google_pay
end
But you can’t use functions for pattern matching.
Our solution
Now what we came up with, but maybe shouldn’t do: Macros
defmodule PaymentProvider do
defmacro __using__(_opts) do
quote
@google_pay "google_pay"
@paypal "paypal"
end
end
end
With this we could improve our AppFailure
like this:
defmodule AppFailure do
use PaymentProvider
...
defp authorization_failed(payment_provider) do
case payment_provider do
@paypal ->
{:unprocessable_entity,
"Please do this and that to fix the problem."}
@google_pay ->
{:unprocessable_entity,
"Open the Google Pay App to update your card details."}
end
end
end
And also use it to pattern match in the function signature:
defmodule Request do
use PaymentProvider
defp payment_authorization(%{
payment_authorization_provider: @paypal,
payment_authorization_attributes: %{"fetching" => some, "provider" => specific_data}
}) do
...
end
end
The refactoring the code with the macro felt better than the initial version with the same strings throughout the code. But we would really like to hear, what you all think about this approach, as we are really hesitant as soon as we try to achieve something with macros and start wondering where we took a wrong turn.
Looking forward to read your thoughts and thank you for reading anyway!