Without knowing the details of the remaining architecture, it’s always a bit difficult, but to play along and suggest a native one.
I think there is several valid approaches. The diagram kinda looks like it could be codified as a sort of state machine to me, and because I’m more familiar with the structure of a genserver than with a gen_statem, I would model a genserver to behave like one, specially now that we have {:continue, _} callbacks, since they produce basically a “handle switch” (although I would probably take the excuse to dig into gen_statem for scientific purposes).
Assuming “prefs” are stored in a database record for the user, I would probably have a genserver being spin up for a user on login, that loads those prefs as part of its state (probably timing out after N minutes of inactivity as well as when the user logs out), and has one callback for updating the prefs. Wrap the events that may trigger a notification on a function/task/process that either finds the running genserver for the user, or spins up one, and sends it the %Event to process.
This setup needs to account for the fact that a user might change their preferences while “their” genserver is already running, that’s why the callback to update those preferences would be necessary. This means that the specific action that updates a user preference would need to send a message to their genserver besides updating the db record.
I would then model the genserver callbacks as state transitions (with :continue, again a statem would probably be the technically correct choice)
So just to illustrate, perhaps after init you would have a state reflecting this:
{user, DND, user_prefs, device_prefs}
{user_id, true | false, %{"channel_1" => :everything, "channel_2" => :nothing}, %{"device_1" => :never}}
(and you could also prep up the prefs in specifically designed structures, instead of bare unstructured maps)
And the events themselves I would probably also wrap in their own dedicated structures, like %ChannelEvent{channel: "channel_1", type: :mention, payload: ...} | %PrivateEvent{...}
, or a single %Event{}
like struct if it worked fine for the majority of events
def handle_info({:maybe_notify, %ChannelEvent{channel: channel} = event}, {_, false, u_prefs, _} = state) do
case u_prefs[channel] do
:everything -> {:noreply, state, {:continue, {:device, event}}}
:nothing -> {:noreply, state}
:perhaps -> {:noreply, state, {:continue, {:channel_perhaps, event}}}
end
end
# ideally you would get away with matching just the cases where a notification should be sent, and catching all others in a :noreply handle
def handle_continue({:device, event}, {user_id, false, _, d_prefs} = state) do
Enum.each(d_prefs, fn
({_, :never}, acc) -> :noop
({device, :everything}) -> Task.start(fn -> Somemodule.persist_and_send_push_notification(user_id, event, device) end)
end)
{:noreply, state}
end
# other specific :continues, bearing in mind that you can loop into any :continue if needed, as if it was a statem
Basically have a heavier init but afterwards (assuming when a user becomes active it will probably receive notifications in a continuous fashion, so this would be an advantage) a process already prepared to handle them, that goes through a sequence of handle_continues until it resolves.
There would be a non-trivial amount of details taking into account that flowchart (like, do you also need to decide if the notifications are persisted, if they need to be flagged as read/unread, etc), so in the end, I think, the choice would be the familiar “it depends”, but I would try first to see if I could fit it into something like this because in my mind it makes sense.