Search
Search docs, blog posts, and ecosystem packages with citations.
Enter a query to see grounded citations.
We can't find the internet
Attempting to reconnect
Search docs, blog posts, and ecosystem packages with citations.
When a plain GenServer is enough and when Jido's data-first model pays off.
GenServer is the natural starting point for stateful processes in Elixir. It gives you a process, a mailbox, and state management - which is enough for many problems. Jido builds on top of OTP by separating what an agent decides from how the runtime executes it.
GenServer provides process isolation, message serialization, supervision tree integration, and hot code upgrades out of the box. For single-responsibility processes with straightforward state, it is the right choice.
You define callbacks, receive messages, and return updated state. The BEAM handles the rest. This model scales well when each process owns one concern.
Problems appear when a single callback mixes validation, persistence, and side effects. Consider an order processing server:
defmodule MyApp.OrderServer do
use GenServer
def handle_call({:process_order, order}, _from, state) do
case validate_order(order) do
{:ok, valid} ->
{:ok, _} = MyApp.Repo.insert(valid)
MyApp.Mailer.send_confirmation(valid)
new_state = Map.put(state, :last_order, valid)
{:reply, {:ok, valid}, new_state}
{:error, reason} ->
{:reply, {:error, reason}, state}
end
end
end
If the database write succeeds but the email fails, what state is the process in? The state update already happened in memory, but send_confirmation raised. The supervisor restarts the process, but the side effect is partially applied.
This is not a GenServer flaw. It is a design pressure that grows with every concern you add to a callback. Validation, persistence, notification, and state transition are four separate responsibilities sharing one function body.
With Jido, the same logic becomes a pure action that returns state changes and directives:
defmodule MyApp.ProcessOrder do
use Jido.Action,
name: "process_order",
schema: Zoi.object(%{
order_id: Zoi.string(),
total: Zoi.float()
})
@impl true
def run(params, _context) do
signal = Jido.Signal.new!(
"order.processed",
%{order_id: params.order_id},
source: "/orders"
)
{:ok, %{last_order: params.order_id, status: :processed},
%Jido.Agent.Directive.Emit{signal: signal}}
end
end
The state transition is pure. Side effects are described as directives, not executed inline. The runtime decides when and how to apply them.
cmd/2 with any agent state and assert on the returned state and directives. No process needed. Use a plain GenServer when you have a single-responsibility process with simple state and no composition needs. A cache, a rate limiter, or a connection pool are good examples.
Use Jido when you need multi-step workflows, validated state transitions, effect isolation, pluggable strategies, or AI integration.
A rule of thumb: if your GenServer’s handle_call does more than one thing (validates + persists + notifies), Jido’s separation will pay off.
Jido does not replace GenServer. It builds on it. Jido.AgentServer is a GenServer. You still get supervision, process isolation, and all the BEAM guarantees.
Jido adds structure on top of OTP: typed state, deterministic transitions, signal routing, and directive-based effects. The process model stays the same. The execution model becomes composable.