Setting boundaries between resolvers/controllers and contexts

Still struggling a bit with patterns for context boundaries and hoping for some advice.

I frequently have code like this in my Absinthe resolvers for mutations (although the pattern would be exactly the same if it were a standard REST controller):

  def password_reset(_, params, _) do
    with {:l1, {:ok, _recaptcha_response}} <- {:l1, Recaptcha.verify(params.recaptcha)},
         {:l2, {:ok, username}} <- {:l2, Accounts.username_or_email_to_username(params.username_or_email)},
         {:l3, %User{} = user} <- {:l3, Accounts.get_user(username)},
         {:l4, {:ok, reset_token}} <- {:l4, Accounts.create_password_reset(username)} do
      Email.send_password_reset_email(user.email, reset_token)
      {:ok, %{errors: []}}
    else
      {:l1, _error} ->
        {:error, "Recaptcha Error"}
        # All other else clauses go here
    end

Of course this works great but I’ve this nagging feeling that most of this logic should exist in Accounts.create_password_reset/1 and only checking the recaptcha should stay in the resolver/controller since Recaptcha is a different app/dep and a web layer concern. It seems to make sense that the resolver/controller be the glue that calls different contexts to keep the contexts from being coupled to each other but calling Accounts 3 times here is wrong.

Maybe the resolver/controller should be like this:

  def password_reset(_, params, _) do
    with {:l1, {:ok, _recaptcha_response}} <- {:l1, Recaptcha.verify(params.recaptcha)},
         {:l2, {:ok, {email, reset_token}}} <- {:l2, Accounts.create_password_reset(params.username_or_email)} do
      Email.send_password_reset_email(email, reset_token)
      {:ok, %{errors: []}}
      # ... else clauses go here

Since this resolver is the only consumer of this context API I can foresee does it even matter or is this all just personal preference? Or am I missing something? Thanks!

2 Likes

I don’t have a definitive answer for you. My approach is to start with having the code in the controller, and if I need similar functionality somewhere else, I refactor it into existing context modules or utility modules.

I generally apply this pattern when writing my code: Write the logic where I need it, and move it if it is beneficial (easier to read or allows sharing of logic).
This approach allows me to quickly get the logic written instead of trying to design for future use-cases.

But obviously, this is just my personal preference.

2 Likes