liveBook · Manning

Server process

As explained in Elixir in Action, Third Edition

A server process is a concurrent process that encapsulates private state and communicates with other processes through messages.

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

How Elixir in Action, Third Edition covers it

How deep it goes
Builds raw spawn-and-receive loops, mutable recursive state, generic callbacks, communication trade-offs, and sequential bottlenecks before introducing GenServer.
Examples
Demonstrates sum and database servers, a stateful calculator, a TodoServer, a generic callback server, and a sequential bottleneck.
What it assumes
Assumes basic Elixir functions and processes, then develops message passing, recursive state, interfaces, callbacks, and request semantics.
What you can do after
Builds raw stateful services and refactors them toward generic ServerProcess or GenServer implementations.
How it connects
Connects processes, message passing, immutable data structures, tail recursion, GenServer, OTP, supervision, back pressure, and ETS.

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Elixir in Action, Third Edition. The section links point to the parts of the book it draws on.