Hacker's Handbook


Resource Owners: One Process, One Piece of State
The boring, reliable center of most good systems
Posted: 2025-11-24

The Gnome Who Guards the Chest of Gold

In the gnome village, not every gnome runs around performing tasks. Some stay in one place, holding on to something important. A chest of gold. A ledger. A cart. A user session. They don't roam, they don't multitask, and they are not easily distracted. They are predictable by design.

A happy gnome
holding the key to the treasure chest.

Everything that touches that piece of state goes through this gnome. No exceptions. No secret shortcuts. No shared cupboards where anyone can slip in and "just update one field".

The Resource Owner archetype is simple: one process owns one thing.

Everyone else sends requests. The Resource Owner decides what happens. It enforces the rules, updates the state, and ensures that invariants stay invariant. If two other gnomes want to withdraw from the same chest of gold at the same time, this gnome decides the order. There are no races, only queues.

A Resource Owner doesn't need locks, because no one else is allowed to touch the chest. It doesn't need coordination protocols, because the mailbox does the serialization.

You can send as many messages as you want. The Resource Owner handles them one at a time. It is boring, reliable, and honest work. In other words: the foundation of most good systems.

Workers run around doing tasks. Resource Owners sit still and make sure those tasks don't break anything.

If you've ever wondered why BEAM systems behave sensibly under concurrency pressure, this is the reason. There is always a gnome with the key to the chest, and everyone in the village respects that arrangement.

What Exactly Is a Resource Owner?

A Resource Owner is the simplest idea that people consistently overcomplicate.

It is a process whose entire job is to own one piece of state. All interaction with that state happens through the process.

Because the Resource Owner has a mailbox, it automatically serializes access. Two concurrent updates become a queue, not a race. The process handles one message at a time, in order, and applies the change cleanly.

OTP encourages you to write these as a gen_server. For very simple owners you don't have to. A simple receive loop works just as well. The behaviour module is convenience, not magic.

For anything more complex, a gen_server gives you a lot for free: code upgrades, casts and calls, and a familiar interface.

A Resource Owner is not a Worker. Workers do things: calculations, I/O, external calls. Resource Owners protect things: the chest of gold, the cart, the payment intent, the ledger entry.

If a Worker needs to modify a resource, it does not touch the state itself. It sends a message to the Resource Owner. The Worker may be short-lived. The Resource Owner is the stable part of the system.

A Minimal Example

Here is the smallest useful Resource Owner: a process that owns a counter.

-module(counter_owner).
-export([start_link/0, increment/1, value/1]).

start_link() ->
    spawn_link(fun() -> loop(0) end).

increment(Counter) ->
    Counter ! {self(), increment},
    receive
        {Counter, Reply} -> Reply
    end.

value(Counter) ->
    Counter ! {self(), get},
    receive
        {Counter, V} -> V
    end.

loop(State) ->
    receive
        {From, increment} ->
            New = State + 1,
            From ! {self(), ok},
            loop(New);

        {From, get} ->
            From ! {self(), State},
            loop(State)
    end.

This tiny process gives you:

  • One owner of one integer.
  • Serialized access without locks.
  • Predictable updates.
  • No race conditions.

Ten, a hundred, or a thousand increments can arrive concurrently. They all go through the same process. The mailbox turns concurrency into a queue.

This is the smallest form of a Resource Owner. The larger forms follow the same principle, just with more interesting state.

Resource Owners come in two flavours. The first group are Entity Owners. The second are Aggregators.

Entity Owners

Entity Owners represent logical domain state. They are the small, polite gnomes guarding individual pieces of your system's model:

  • a socket or DB connection
  • a shopping cart or user session
  • an order or payment intent
  • a chat room or game entity

If you can point at it in your domain and say "this thing has identity and state," it can probably be represented as an Entity Owner.

These processes are:

  • Long-lived and tied to an ID
  • Started when needed, shut down when finished
  • The single authority on the rules of that entity (for example, "a balance must not go negative")
  • A natural fit for the BEAM, because the mailbox gives you serialization without locks, and the process gives you isolation without drama

A Shopping Cart Owner

Here is a simple cart owner. It holds items, enforces quantity rules, and exposes the current state on request. The API uses the cart ID directly, so callers don't need to track PIDs. One process per cart ID. If you know the ID, you know the owner.

-module(cart_owner).
-behaviour(gen_server).

-record(state, {id, items = #{}}).

-export([start_link/1, add_item/3, get_items/1]).
-export([init/1, handle_call/3, handle_cast/2]).

start_link(CartId) ->
    gen_server:start_link({local, {cart, CartId}},
                          ?MODULE, CartId, []).

add_item(CartId, ItemId, Qty) when Qty > 0 ->
    gen_server:call({cart, CartId}, {add, ItemId, Qty}).

get_items(CartId) ->
    gen_server:call({cart, CartId}, get_items).

init(CartId) ->
    {ok, #state{id = CartId}}.

handle_call({add, ItemId, Qty}, _From,
            #state{items = Items} = St) ->
    Current = maps:get(ItemId, Items, 0),
    NewItems = Items#{ItemId => Current + Qty},
    {reply, ok, St#state{items = NewItems}};

handle_call(get_items, _From,
            #state{items = Items} = St) ->
    {reply, Items, St}.

handle_cast(_Msg, St) ->
    {noreply, St}.

In real systems you would probably use a process registry (gproc, pg, or a via callback) instead of {local, ...}, but this is enough to show the idea.

Two concurrent "add item" requests arrive for the same cart. The mailbox queues them. The first completes, updates the map, and replies. The second sees the updated state and proceeds. No locks, no retries. Just order.

This is why Entity Owners feel like cheating when you first encounter them. You stop writing defensive code and start writing real logic.

Lifecycle

Entity Owners are not global daemons. They exist only when needed.

  • When the first request comes in, a supervisor starts the process.
  • When the entity is complete, idle, or expired, it shuts down.
  • If the process crashes, supervision restarts it, either fresh or from persisted state.
  • Because its state is isolated, a crash only affects that one entity.

This is the kind of failure you want in production: local, predictable, and obvious.

Pitfalls

Entity Owners are simple. People make them complicated. Here are the usual ways:

The God Entity. One process ends up owning far too much: user profile, cart, settings, metrics, half the application. This becomes a serialization bottleneck. Also: painful to debug.

A backpack full of boulders. The process holds megabytes of state: giant maps, binary blobs, or entire chat histories. Result: GC pauses, high memory usage, and slow everything. Move big state to ETS or external storage.

Doing I/O inside the entity. The process starts calling remote APIs, writing to disk, or running SQL queries. This blocks the mailbox, inflates response times, and converts a clean design into a subtle disaster.

The fix is always the same: Entity Owners guard state. Workers do work. Keep them separate.

Entities with Lifecycles

Some entities have a clear lifecycle: created, authorized, captured, cancelled. For those, gen_statem is often a better fit than gen_server. The Resource Owner idea is the same (one process owns the intent) but the behaviour makes state transitions explicit. I will cover that pattern in a later post.

Aggregators

Aggregators hold a derived or accumulated state, not a domain entity.

Examples:

  • metrics collector
  • sliding window counters
  • real-time rollups (moving averages, sensor data)
  • batch builders
  • log aggregators before flushing to storage

Properties:

  • Often time-based, window-based, or count-based.
  • State is constantly updated.
  • Output is periodic or triggered.
  • Often respond to queries for current aggregated status.

A Simple Metrics Owner

-module(metrics_owner).
-behaviour(gen_server).

-record(state, {count = 0, sum = 0}).

-export([start_link/0, record/1, get_mean/0]).
-export([init/1, handle_call/3, handle_cast/2]).

start_link() ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

record(Value) ->
    gen_server:cast(?MODULE, {record, Value}).

get_mean() ->
    gen_server:call(?MODULE, get_mean).

init([]) ->
    {ok, #state{}}.

handle_cast({record, Value}, #state{count = C, sum = S} = St) ->
    {noreply, St#state{count = C + 1, sum = S + Value}};

handle_cast(_Msg, St) ->
    {noreply, St}.

handle_call(get_mean, _From, #state{count = 0} = St) ->
    {reply, undefined, St};

handle_call(get_mean, _From, #state{count = C, sum = S} = St) ->
    {reply, S / C, St}.

Callers send latency values with record/1. The owner accumulates count and sum. Anyone can ask for the current mean. The state stays bounded, the interface stays simple. In real code you would want a better return type than undefined, but you get the idea.

Pitfalls for Aggregators

  • Unbounded state growth. Sliding windows need pruning. Batch builders need flush triggers.
  • Holding giant binaries. If you keep references to large binaries, the BEAM cannot reclaim them. Copy what you need.
  • Mixing aggregation with heavy computation. Keep the owner thin. Offload expensive work to Workers.

Pooled Resources

A pooled "Worker" holding a DB connection, socket, or crypto context is actually a Resource Owner. It holds state (the connection), but it behaves like a Worker while serving a request. The pool provides concurrency control; the process provides state semantics.

This is one of the few legitimate role combinations. Keep the pooled process thin. If it starts doing heavy logic, split it: one process owns the connection, another does the work.

Summary

Resource Owners give you correctness by construction.

  • Entity Owners: one process per domain entity.
  • Aggregators: one process per rolling or accumulated state.
  • Pooled resources: a valid hybrid, but keep them thin.

Prefer simple ownership over shared state or locking systems. The mailbox is your serialization primitive. Use it.

Back to blog index. Tip: Use the CONFIG menu (⚙️) to adjust font size and reading direction.


Happi Hacking AB
KIVRA: 556912-2707
106 31 Stockholm