First of all, these are all "OTP Design Principles" (and supported with a standard library), they are all wrappers (or better put, abstractions) on top of basic Erlang primitives like processes. This means they are not the only way to program in Erlang (hence Elixir), but they're shared by the community and backed by decades of battle-tested real-world systems. So you should use them too.
GenServer
is usually considered as the basic runtime building block for applications implemented with Elixir. Although in fact it's a wrapper around the lower-level Erlang Process primitives, however, GenServer
provides many advanced features like debugging and tracing in a standard interface. Basically, you would usually use (many) GenServer
to manage state and "do actual work" in your application.
Supervisor
, as the documentation suggests:
A supervisor is a process which supervises other processes, which we refer to as child processes. Supervisors are used to build a hierarchical process structure called a supervision tree.
So in a sense, it "manages" GenServer
s, but also other Supervisor
s (and other Erlang Process abstractions, if they implement proper interface). You usually don't put any custom logic in Supervisor
modules, partly because their role is very focused - just manage child processes' life cycles. But also to make it less prone to errors (introduced by your changes).
Say it in another way, Supervisor
doesn't do any actual "work" for your application, it's just used to "layout" the architecture of a system.
Compared to the others, Application
is not necessarily a "runtime concern", as the Erlang documentation suggests:
... make the code into an application, that is, a component that can be started and stopped as a unit, and which can also be reused in other systems.
So Application
is really about bundling stuff into a "unit", and real systems are usually composed of many such units - for example whenever you run some Elixir code, there is also an "elixir" Application
which runs several processes such as elixir_code_server
:
More importantly, Application
is THE way to share (reuse) code in OTP design principles. When you define an application, it can have a "root supervisor", which is started together with the application. Or it can also just have functional codes, without running any processes at all, like a JSON library. Either way, to reuse code across different systems, they need to be packaged into Application
s.
Finally, I'd suggest give this a read: https://ferd.ca/the-zen-of-erlang.html it explained (at least for me) how we go from basic (Erlang) processes, to supervisors (and supervision tree), and a little bit of OTP applications. (And how we should approach programming within BEAM)