We cannot anymore click on the transfer call and have a look at the implementation. Instead, we need to go through the entire pattern match inside handle_message to see where the case for Transfer is handled. If there are several message variants, the actual code dealing with transfers is probably tucked away in an internal function somewhere else I agree that there are more level of indirections when using an actor model. Below is an example in Erlang from https:// github.com/francescoc/scalabilitywitherlangotp/blob/master/ch3/frequency.erl where the loop method that processes messages calls other methods depending on the message: loop (Frequencies) -> receive {request, Pid, allocate} -> {NewFrequencies, Reply} = allocate(Frequencies, Pid), reply(Pid, Reply), loop(NewFrequencies); {request, Pid , {deallocate, Freq}} -> NewFrequencies = deallocate(Frequencies, Freq), reply(Pid, ok), loop(NewFrequencies); {request, Pid, stop} -> reply(Pid, ok) end . %% The Internal Helper Functions used to allocate and %% deallocate frequencies. allocate ({[], Allocated}, _Pid) -> {{[], Allocated}, {error, no_frequency}}; allocate ({[Freq|Free], Allocated}, Pid) -> {{Free, [{Freq, Pid}|Allocated]}, {ok, Freq}}. deallocate ({Free, Allocated}, Freq) -> NewAllocated=lists:keydelete(Freq, 1 , Allocated), {[Freq|Free], NewAllocated}. But I personally find it very readable. And when using behaviours such as gen_server, I still find the code readable (https:// github.com/francescoc/scalabilitywitherlangotp/blob/master/ch4/frequency.erl): handle_call ({allocate, Pid}, _From, Frequencies) -> {NewFrequencies, Reply} = allocate(Frequencies, Pid), {reply, Reply, NewFrequencies}. handle_cast ({deallocate, Freq}, Frequencies) -> NewFrequencies = deallocate(Frequencies, Freq), {noreply, NewFrequencies}; handle_cast (stop, LoopData) -> {stop, normal, LoopData}. allocate ({[], Allocated}, _Pid) -> {{[], Allocated}, {error, no_frequency}}; allocate ({[Res|Resources], Allocated}, Pid) -> {{Resources, [{Res, Pid}|Allocated]}, {ok, Res}}. deallocate ({Free, Allocated}, Res) -> NewAllocated = lists:keydelete(Res, 1 , Allocated), {[Res|Free], NewAllocated}. One of the benefits of this code structure is that tracking and logging the changes of the actor state can be done in a single place (the infinite loop that processes messages), and such a feature is even avalaible for some behaviours defined in the OTP library in Erlang/Elixir.