GenServer you can hold state and build a layer of abstraction around it for accessing the state.
Just consider some software that does manage bank accounts.
When you just read and write into an ETS, then you can’t guarantee reads and writes happen in order. You can’t even guarantee atomicity of some requests, which would blow up consistency of your database.
GenServer on the other hand side, does guarantee, that everything happens in the right order, as well as atomicity of transfering money from one account to another. At least it does seem like this from external processes.
You could write a wrapper around an ETS, but as soon as someone else finds out the name of the table, he can at least read at will, while there is no way to read from an
GenServers state without using its official API and waiting for beeing the next one beeing processed.
Also one thing to consider when trying to decide between them, is that the state of a
GenServer is garbage collected automatically, while you have to remember yourself to drop columns in an ETS when they are not needed any more.
Next thing to consider is, that an ETS is only accessible from the node it is running on (IIRC), while you can send messages to an
GenServer across nodes.
Last but not least, there might be kinds of states that do not fit into a table like structure as an ETS provides it (or would generate a huge overhead in flatteing it down until it fits), such data might fit perfectly into a single
GenServer. (Or the state itself is so small, that a table would be overkill; consider a simple integer-value that does count something)
From erlang point of view, most of the time, I create a
GenServer first and just use it. When I realize that it is a bottleneck, I keep its API and create an ETS which I do access over the
GenServers API. This way, most of the time I do only need to change code in the
GenServer when I need to restructure the data in the state (beeing it passed around as state or beeing it stored into an ETS). When working with an ETS directly from the beginning, you might to change code across multiple modules to make it fit your new data structure. If your test coverage isn’t good, you might even miss some parts until you went into production and your clients start to loose money and blame you