While I agree, the article doesn't really give concrete examples.
Elixir is layered, making it easy to learn and master. You can get pretty far with Phoenix without ever understanding (or even knowing about) the more fundamental building blocks or the runtime. In large part, this is because of its ruby-inspired syntax. You'll have to adjust to immutability, but that's pretty much it.
Then one day you'll want to share state between requests and you'll realize that the immutability (which you're already comfortable with at this point) goes beyond just local variables: it's strictly enforced by these things called "processes". And you'll copy and paste a higher-level construct like an Agent or Genserver and add the 1 line of code to this root supervisor that was just a file auto-generated in your project. But that'll get you a) introduced to the actor model and b) thinking about messaging while c) not ever worrying about or messing up concurrency.
Then you'll want to do something with TCP or UDP and you'll see these same patterns cohesively expressed between the runtime, the standard library and the language.
Then you'll wan to do something distributed, and everything you've learnt about single-node development becomes applicable to distributed systems.
Maybe the only part of Elixir which can get complicated are Macros / metaprogramming. But you can get far without ever understanding this, and Phoenix is so full of magic (which isn't a good thing), that by the time you do need it, you'll certainly have peaked behind the covers once or twice.
The synergy between the runtime, standard library and language, backed by the actor model + immutability is a huge productivity win. It's significantly different (to a point where I think it's way more accurate to group Ruby with Go than with Elixir), but, as I've tried to explain, very approachable.
Yeah, elixir provides Registry which is what I use for simple state management or directly use ets.
See below (jswny) for more clarification.
Unfortunately had a bug I found today in which an event_notifier genserver was calling a method that checked against the database for the the state of an Event, to decide whether to send a notification and then update the event to record that notifications were sent and upon successful update send the notifications. But in the query to construct the list of users that should be notified, the User module was not imported at the top of the file and the notifications were failing to send. Of course, if the path had ever been run, it would have obviously failed and been the simplest type of error to rectify. This is why it's generally not an issue and is actually nice as it gives you direct visibility into dependencies. But, on rare exception, when you don't test the path because it's dependent on time-dependent state and are overly confident in the suitability of the untested code you're deploying to production, you get bit by the compiler that quietly warns and lack of static analysis (oh nice writing this comment just made me discover https://github.com/rrrene/credo)
I use both dialyxir and Credo in my projects