How can you modularize a Phoenix Channel

I’m struggling with how to sustainably organize a Phoenix Channel. Even with the concept/design of “Phoenix Is Not Your Application” I find my channels growing very large as the number of potential messages sent to or from a channel increases.

I’ve sometimes wished for a “routing” mechanism similar to HTTP but specific to a channel. So that certain prefixes of incoming channel messages could be handled in a consistent way. Alternatively I may want to just create more channels to spread out the messages, but then the clients need to join multiple channels when they connect to the socket. Which isn’t a huge burden (and is fine performance-wise since subscriptions are cheap), but feels like unneeded complexity.

How have others tackled this issue? Or is it a non-issue?

2 Likes

Can you share your current channel code? it would be easier to say more if we have a real example to discuss. Thanks!

3 Likes

Personally I actually find that big channels/gen_servers aren’t much of an issue as most of the logic is descriptive enough for a quick search to jump straight to where you want to go and with something like the structure view in IntelliJ IDEA it becomes even easier.

If you need to offload some of the logic to somewhere else you could just make modules for the different message types you have in your channels, though, and essentially build module based routing into the channel. I don’t think there’s any shame in having you check the type of an incoming message and routing it to a module that really only contains some handle_that_type_of_message functions, all relating to that area (or context, if you will).

I used to do this slavishly for things like gen_servers even because I thought that it was horrible how big they could get, but I realized that everything is so self-contained and neat in a gen_server that I wasn’t accomplishing that much anyway. It’s either that or Stockholm Syndrome.

With full blown WebSocket APIs I also dislike them getting too big so I split them into sub-channels with a tiny bit of pattern matching, this way I can have the handlers separate (they still don’t do anything but delegate to context services), that keeps the files reasonably small.

Here is an example of a channel that delegates to subtopics:

alias App.Service.Channel.{Faq, Learning, Search} # etc

  # join clauses
  
  def handle_in("faq:" <> msg, payload, socket), do: Faq.process_message(msg, payload, socket)

  def handle_in("learning:" <> msg, payload, socket), do: Learning.process_message(msg, payload, socket)

  def handle_in("search:" <> msg, payload, socket), do: Search.process_message(msg, payload, socket)

 # etc

Frontend can then send a message such as search:find with some payload, the channel will process it and call process_message in Search service with find message.