Designing Elixir Systems With OTP - isn't the integration pattern interfering with business logic?

Hello! :wave:

I recently completed the book and highly recommend it to anyone looking to deepen their understanding of OTP (especially if you’re relatively new to Elixir and Erlang, as I am).

Now, onto my question :grin: The authors’ approach to integrating the persistence layer using a poncho project—where a persistence_fn function is passed to the core project—raised an interesting thought for me.

The original implementation of the :answer_question handler creates a new Response, uses it to advance the Quiz (by calling answer_question and select_question), and concludes the quiz if no questions remain. Here’s the code:

def handle_call({:answer_question, answer, fun}, _from, {quiz, email}) do
  response = Response.new(quiz, email, answer)

  quiz
    |> Quiz.answer_question(response)
    |> Quiz.select_question()
    |> maybe_finish(email)
end

defp maybe_finish(nil, _email), do: {:stop, :normal, :finished, nil}

defp maybe_finish(quiz, email) do
  {
    :reply,
    {quiz.current_question.asked, quiz.last_response.correct},
    {quiz, email}
  }
end

After incorporating the poncho project and modifying the handler to expect an external function, the implementation changes as follows:

def handle_call({:answer_question, answer, fun}, _from, {quiz, email}) do
  fun = fun || fn r, f -> f.(r) end
  response = Response.new(quiz, email, answer)

  fun.(response, fn r ->
    quiz
    |> Quiz.answer_question(r)
    |> Quiz.select_question()
  end)
  |> maybe_finish(email)
end

defp maybe_finish(nil, _email), do: {:stop, :normal, :finished, nil}

defp maybe_finish(quiz, email) do
  {
    :reply,
    {quiz.current_question.asked, quiz.last_response.correct},
    {quiz, email}
  }
end

By default, fun retains the original behavior, but now the user can pass a custom fun. If the passed fun does nothing, Quiz.answer_question and Quiz.select_question will no longer execute, preventing the quiz from progressing.

Am I correct in understanding that this integration effectively allows a “third party” (the poncho project) to interfere with the core business logic? Or am I overlooking something? :thinking:

PS: I haven’t contacted the authors to paste that code in here. If it’s against the rules, I am happy to take it down.

That’s an interesting approach to adding a degree of flexibility! I didn’t read the book but now I’m curious to learn more about what’s going on.

Does the integration layer invoke the core, or is there an orchestrating layer that wires things together, e.g., controllers, or live views?

Anyhow, that’s a neat but a sharp trick as it opens up a possibility of the core to depend on the integrations because integrations are allowed to reach the core’s primitives.

Absolutely! I highly recommend this book to you if you want to learn more about using OTP to orchestrate all the layers in an Elixir project.

The authors wire everything together in a layer they call “boundary”, where the different processes interact with each other.

Thanks for the support and high praise.

You have it right. As an author, you are usually wrestling with the balance of making the example meaty enough to be meaningful versus needing so much code that the examples go on and on for pages. It’s a difficult tightrope to walk.

Do I think this code is the best it could be? No. The function should probably be executed in some error isolating construct and the return value be ignored if it doesn’t conform to some strict protocol. Sadly, that would just require more code. As it is, that book is on the high side of what a lot of folks tolerate as far as lengthy examples go.

It’s worth noting that this design of persistence is not at all common in our community. I don’t expect you to run into a lot of examples like this. It’s far more common to see database constructs driving the initial design of the core business code, in my experience. You can read more about those tradeoffs in this thread.

I will say this, though. I stand by the value of the approach in the book. I wish more people would consider it. I acknowledge all of the tradeoffs discussed in the thread above. However, I often find that the state I want in the application differs significantly from how data is stored in long term persistence. For example, if I am managing a chess game, the running application cares about the current state of the board and where all the pieces are. But as a rich history of the game of chess has taught us, what we want to persist is the list of moves that can be used to recreate and study the game. These are two very different needs and I believe there’s a lot of value in separating those concerns.

That’s just one person’s opinion. Hopefully it will give you some fun new ideas and help you discover better designs for your own projects.

Thanks again for the kind words about the book.

8 Likes

Hi there, James! :wave: Thanks for hopping on the thread, I really appreciate it :smile:

Yeah, that makes sense and, I totally understand. In all honesty, when I posted that question I was coming from a place of ignorance, not criticism (and I’m still in that place, I haven’t yet escaped that place :grin:). Moreover, one does not get many chances of saying “poncho project” in public, I couldn’t let this opportunity slip by!

Thanks for that as well. I found the code in the book profoundly insightful, and I hope I haven’t come across as judgmental or overcritical. My goal was to understand it better, not to nag about the code. After reading it I was left with a feeling of having missed something important that gnawed at my head, and went straight to the community to get the knowledge.

Thanks for sharing!

1 Like

I second the praises for the book. It changed the way I think about designing software in Elixir and as a whole.

Unfortunately, for almost 4 years of professional Elixir experience, I haven’t once seen an Elixir project designed the way presented in the book. It’s a shame.

I feel like a big reason for people to go with database driven designs is doing „web“. Http is a stateless protocol and we‘re expected to always be able to „cold start“ serving requests through http. Adding stateful handling will always feel like more work in such an environment (it is) and more work needs justification. Justification however can only be provided having seen the potential benefits of an stateful approach.

LiveView is imo a great technology to explore constraints and benefits even in a web space, where you have static stateless renders as well as stateful connected processes.

I‘d phrase this differently. The provider of the implementation doesn‘t „infer“ with the business logic. It „provides“ the business logic. If it provides one where progress is impossible it just is this way. It‘s no other code‘s job to prevent this.

2 Likes

Hi @LostKobrakai, thanks for your contribution! :wave: I find what you said interesting. I have a hard time wrapping my head around the idea of giving a dependency the power to change core behavior of the application, so I’d lean more into what @JEG2 proposed (that, if it weren’t a book, he’d isolate the persistence-related portion of the code). Would you expand on that? It sounds like you have a reason for saying that :smiley:

:wave:

No offense taken.

I have no notes. :smile:

Thanks you so much!

2 Likes

Amen to that! I really wish people would use data models preserving context. The SQL model gives a lot of convenience but the costs are not always a part of the initial discussion.

And as you said the app/db impedance mismatch leading to anaemic domain models is a story that happens more often than not.

4 Likes