How would you implement Slack's notification flowchart in Elixir?

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.

1 Like