GenServer With State Management

In the previous lesson, you saw how a GenServer holds simple state and handles synchronous (call) and asynchronous (cast) messages. As a quick reminder, calls wait for a reply, while casts do not. Today, you will build on that pattern to manage richer, structured state using a BankAccount server that tracks a balance and a transaction history.

Registered Name vs. PID-based GenServer APIs

In Unit 1, we used a registered name (name: __MODULE__) for our GenServer. This lets you call functions like Stack.push(1) without passing a PID, which is convenient for a single, global instance of a server.

In Unit 2, you’ll notice we switch to PID-based APIs—for example, BankAccount.deposit(account_pid, 100). This approach is necessary when you want to run multiple independent GenServer instances (such as one bank account per user), or when you use tools like the Registry for dynamic process lookup.

When to use each:

  • Registered name: Use when you only need one instance of a server in your system (e.g., a single global stack or logger).
  • PID-based: Use when you need many independent servers (e.g., one per user, session, or resource), or when you want to supervise and manage them dynamically.

Understanding this distinction will help you design scalable, maintainable Elixir applications!

Start the Server and Initialize Structured State

We begin by defining the module, starting the process, and initializing a map-based state that includes both balance and transactions:

Explanation:

  • use GenServer brings in the GenServer behavior.
  • start_link/1 starts the process and passes the initial balance to init/1. Note that name: __MODULE__ is not written here, which is why each call to start_link/1 returns a unique PID.
  • init/1 sets the state to a map with two keys: :balance and :transactions. This is a scalable pattern for managing multiple related fields.
Public API and State-Changing Calls

We expose a small, clear API. As a reminder from the previous lesson, GenServer.call/2 is synchronous, which fits operations where you need a result or an error:

Explanation:

  • All three functions use calls because the caller needs an immediate reply: the new balance, an error, or the current balance.

Now, let’s see how those calls are handled inside the server. Each callback updates or reads the immutable state and returns a new state when needed:

Explanation:

  • Deposit: computes a new balance, prepends a transaction entry, and replies with {:ok, new_balance}.
  • Withdraw: checks funds first. On success, updates the balance and transactions; otherwise, replies with {:error, "Insufficient funds"} and keeps the state unchanged.
  • Get balance: replies with the current balance without changing the state.
Run and Observe Results

Finally, start the server and exercise the API:

Explanation:

  • We start with 1000, deposit 500, withdraw 200, and read the final balance.
  • IO.inspect/1 shows replies, including the {:ok, new_balance} tuple and any errors.
  • Process.sleep/100 ensures the script does not exit before processing finishes.
Summary and Next Steps

You have extended your GenServer skills from a simple list to a real stateful service with validation and a transaction log. You defined a public API, handled synchronous calls, and produced new immutable state on each operation. When you are ready, jump into the practice section and apply these patterns to solidify your understanding.

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal