Best practices for GenServer initialization with IO involved

Hello!

I am pretty new to Elixir and OTP platform in general, so this might be very stupid question :slight_smile:

I want to build a simple parcel tracking service for myself and my friends. Since I am just learning Elixir and functional programming in general, I want to avoid big frameworks as much as possible. Currently I am only using Ecto for the database connection and data validation, and Req for external API calls.

The idea of the application is that user can send a tracking number to a Telegram bot, and it will periodically fetch the parcel state and notfiy user if there are new events. It will be very low-concurrent application, since it’s probably will be used by one or two people, but I still want to use best practices, just for personal education. So I would be very glad to hear experienced developers’ opinion on the architecture and approaches I am going to use.

I want to create a GenServer per parcel. It is going to manage the persistence, status updates, and stuff like that. I had the idea that I can just pass the tracking number to GenServer, and let it handle the initial validation by itself. By validation I mean a 3rd party API call, to verify that the tracking number is correct.

First, I am not sure how to implement the initialization. I’ve read that it is not a good idea to do IO inside init, since it is blocking the supervisor, so I am doing it via handle_continue. However, this raises a problem: how do I let the client know that the tracking number they passed is incorrect? I can obviously validate it outside of GenServer, but it feels wrong, since I want to avoid calling that API if I can, and there’s no point in calling the API if I already validated that the certain tracking number is correct, and saved it to the database.

Another thing, is how do I handle the persistence? Currently I plan to have a :timer that will send messages to Parcel server every hour, and Server will call an API, check if there are new tracking events, and save them to the database and its internal state. Is it a correct approach?

Third, who should manage subscriptions? For example, two users want to track the same parcel. Should the list of clients also be stored in the Parcel server, or should I offload this to some other process?

Thanks in advance for any advices :slight_smile:

3 Likes

Here’s a very favorite article:

TL;DR you can immediately return from a GenServer.call function with {:noreply, ...} and then in an asynchronous task, spawned in the same function call, you can reply to the request to the caller. For that you need a process that sticks around to receive the reply however – make sure of that in your application if you go with this approach.

You can just have a record in a DB table f.ex. last_checked_service_xyz_at=2026-03-01T21:26:48.378612Z and have a GenServer that periodically wakes up, loads the DB record and if the deadline has passed (1h) then it should fetch / update stuff.

You said you don’t want to use libraries which is fair – you can do it just fine with a GenServer that is checking a single table record. But if you don’t hold that requirement too hard, you can just use Oban + its cron feature. Up to you.

That really depends if they are live connections or they are periodically calling your API. For live connections you should look up Phoenix.Presence and/or Phoenix.PubSub, they do these kinds of things well but be warned – it’s not easy to get right on a first go (though with LLM coding agents it might be). Still though, that question is too open and needs clarification before I could tell you something more concrete.

2 Likes

If you only want to make the ā€œverify this number is correctā€ call once before saving it to a DB, it sounds like that shouldn’t be the responsibility of the GenServer.

You could lift that call into the Telegram bot handler, so that the logic looks something like:

  • message comes in
  • call API to check the validity of the tracking number
  • if valid, start the GenServer
  • if not valid, notify the user
1 Like

Thank you! I was thinking about doing something like that, just add a GenServer.call that I will call immediately after initializing Parcel server. From what I understand, handle_continiue is guaranteed to run before any message is processed, so if something went wrong, I know immediately.

That is also correct though, maybe I am overcomplicating it :slight_smile:

I think Telegram bot can technically be classified as ā€˜live’ connection, but not in a same sense as websockets for example :slight_smile:

Again, thanks a lot for the replies!

Yes, that is correct. It’s effectively a post-init / first-message mechanic.

1 Like

That seems to be unnecessarily complex. I would just make one GenServer that handle all parcels. The following is a good read for when to use a process:

https://www.theerlangelist.com/article/spawn_or_not

3 Likes