A server process is a long-running process that waits for messages, handles requests, and optionally maintains private state. Its sequential loop becomes a concurrent service from the outside. section 5.3.1 section 7.4
The basic implementation spawns a process, waits in receive, handles one message, sends a response when needed, and tail-recurses. Waiting suspends the process without consuming CPU. section 5.3.1 section 5.5
Listing 5.1 Long-running server process (database_server.ex)
defmodule DatabaseServer do
def start do
spawn(&loop/0) #1
end
defp loop do
receive do #2
... #2
end #2
loop() #3
end
...
end
Use the minimal database-server loop to see process startup, message reception, and recursive continuation in one small implementation. section 5.3.1
State remains private to the process and evolves by passing a new value into the next loop iteration. Immutable data therefore appears mutable through message-driven side effects. section 5.3.3 section 5.5
Stateful calculator
A calculator starts with state 0, accepts add, sub, mul, and div messages, and answers value requests. Applying 10, subtracting 5, multiplying by 3, and dividing by 5 produces 3.0. section 5.3.3
Listing 5.7 Concurrent stateful calculator (calculator.ex)
defmodule Calculator do
...
defp loop(current_value) do
new_value =
receive do
{:value, caller} -> #1
send(caller, {:response, current_value}) #1
current_value #1
{:add, value} -> current_value + value #2
{:sub, value} -> current_value - value #2
{:mul, value} -> current_value * value #2
{:div, value} -> current_value / value #2
invalid_request -> #3
IO.puts("invalid request #{inspect invalid_request}") #3
current_value #3
end
loop(new_value)
end
...
end
Inspect the calculator loop to see request clauses, response messages, unchanged state for reads, and new state for arithmetic operations. section 5.3.3
Interface functions hide message formats and caller PIDs from clients. A server can encapsulate a TodoList or other complex abstraction while exposing operations through a stable API. section 5.3.4 section 5.3.3
Every server process handles messages sequentially. Concurrent clients do not make one server parallel, so slow handlers create a throughput bottleneck; optimize the handler first, then consider splitting work. section 5.4.1
Generic server abstractions move spawning, looping, state management, and message passing into reusable behavior. GenServer supplies this pattern with callbacks such as init/1, handle_call/3, handle_cast/2, and handle_info/2. section 6.1 section 6.3
Listing 6.2 Handling messages in the server process (server_process.ex)
defmodule ServerProcess do
...
defp loop(callback_module, current_state) do
receive do
{request, caller} ->
{response, new_state} =
callback_module.handle_call( #1
request, #1
current_state #1
) #1
send(caller, {:response, response}) #2
loop(callback_module, new_state) #3
end
end
...
end
Compare the generic loop with concrete callbacks: it receives a request, delegates handling, sends the response, and continues with updated state. section 6.1.2
Choose the communication pattern deliberately:
- Use a call when the client needs a response, consistency, or back pressure. section 6.3 section 7.4
- Use a cast for fire-and-forget responsiveness when the caller can tolerate no delivery or outcome guarantee. section 6.3 section 7.4
- Use separate processes for separate tasks or stateful entities, while keeping each entity’s requests serialized. section 7.0 section 7.4
