Any idea of what code to put in the domain's main file

Context

Generating a phoenix app with name foo, an empty module will be created in the foo/lib/foo.ex file like so:

defmodule Foo do
  @moduledoc """
  Foo keeps the contexts that define your domain
  and business logic.

  Contexts are also responsible for managing your data, regardless
  if it comes from the database, an external API or others.
  """
end

That is what I’m calling the main file.

Question

What code do you put in there or any idea of what could fit?

In my mind it makes sense to put functions that use other functions from the contexts modules to deliver a complete service/feature of the system (e.g. user registration). For example:

defmodule Foo do
  alias Foo.Accounts

  def register_user(params)
    case Accounts.register_user(params) do
      {:ok, user} -> 
        Accounts.deliver_user_confirmation_instructions(
          user,
          &Path.join(params["confirmation_url"], &1)
        )

        {:ok, user}

      {:error, _} = error -> 
        error
    end
  end
end

See any cons about this idea?

You need to remember that calling Foo.bar() is not the same as calling Foo.Accounts.bar()

Mostly my Foo contains delegations to other contexts

Also, I would prefer register_user() not to be tightly coupled to deliver_user_confirmation_instructions(). For example by sending a domain event…

1 Like

The way I see, makes sense to couple functionalities in the Foo instead of spread over the interface (web modules) and leaving this module to be used only in it, thus decoupling the interface from the domain. Internally I would still call functions from the contexts modules to accomplish application demands (async jobs, clean ups, etc…).

What do you call a domain event? Have any materials about it?

It is related to Event Sourcing… You listen to events happening in the system, and react accordingly

It’s not related to web interface

1 Like

To me the functions that would most naturally live here are delegates to every single public function in your domain. This is pretty heavy-handed, though, and I wouldn’t recommend it—I tried it! Otherwise, what you’re suggesting at the very least hurts discoverability. If you think of it from the perspective of someone coming in and only reading the business domain code, there will be auth functions spread out over different context boundaries. I don’t think it’s the worst thing in the world, but to me I would just think there is no point—just add a function to Accounts called register_and_notify_user. I would also change the signature to take user_attrs and confirmation_url separately.

Really in a Phoenix app, I think the best use of this module is documentation. I’m sure there is something that could naturally fit here, but I’ve never come across anything. I think a lot of people get perturbed that this file sits there devoid of code. It doesn’t personally bother me. If you use ex_doc on your project it certainly comes in handy!

1 Like

I personally like to use it similar to the way Phoenix does it with the FooWeb module; As a place to define macros for your app.

A good example would be something like:

defmodule Foo.Accounts.User do
  use Foo, :schema
  
  schema "users" do
    # ...
  end
end

Which Foo can have something like:

defmodule Foo do
  def schema do
    quote do
      use Ecto.Schema
      import Ecto.Changeset

      @primary_key {:id, :binary_id, autogenerate: true}
      @foreign_key_type :binary_id
      @timestamps_opts [type: :utc_datetime_usec]
    end
  end

  defmacro __using__(which) when is_atom(which) do
    apply(__MODULE__, which, [])
  end
end
3 Likes

Oh ya, I’ve actually done this too in the past! I don’t think it’s a bad idea, but I backed-out of it because I realized that the main reason it’s done this way in web is because there is a bunch of code-sharing going on (most html_helpers in the current version). It felt like a bit too much much ceremony with no big benefit over just making MyApp.Schema, MyApp.Context, etc. But again, I don’t think it’s wrong or anything. It does nicely self-document the different “overarching types” of things in the app (I know there is a better word for these things but can’t think of it atm).

The one thing that is weird about it (and I feel that way with the web context too) is that it’s violating boundaries. In a module hiearchy, the children shouldn’t know about the parents. I never actually used this for very long—does it cause any heavy compile time dependencies for you?

I have done it both ways as well (defining a dedicated MyApp.Schema). I don’t have a big preference between both and have not really noticed big compile time differences, or not enough to notice at least.

1 Like

What does this look like in practice? Are you talking about using Phoenix.PubSub to publish/consume events? Something else?

There is this package that does CQRS/ES

I did my own event store…

and a simple demo usage

This allows me to decouple context… instead of doing

def register_user(...) do
  do_work()
  deliver_user_confirmation_instructions()
  notify_by_email()
end

I do

def register_user(...) do
  do_work()
  EventStore.create_event(...)
end

Any context can listen to any event…
register_user() does not need to know what to do when a user register
Adding functionalities is easier, because it does not affect previous code

When You create an event, it is dispatched to listeners

  def dispatch(event) do
    Logger.info("DISPATCH EVENT : #{inspect(event)}")

    ListenersProvider.get_listeners()
    |> Enum.filter(fn {_pid, filter_fun} -> filter_fun.(event) end)
    |> Enum.each(fn {pid, _apply_fun} -> send(pid, event) end)
    :ok
  end
2 Likes

I usually move whatever is on Foo.Application to Foo and update my mix.exs file to reflect that.
if it’s a phoenix app i usually rename FooWeb to be only Web and I rename the remaining stuff in the foo folder to be core and have a Core namespace.

but that’s because i really don’t how namespaces work in the default generators for plain mix or phoenix.

2 Likes

I can’t wait to read more into this. It looks neat, thank you for sharing!