GenServer - jak Elixir zamienia procesy biznesowe w żywe obiekty w pamięci

W tradycyjnym systemie webowym proces biznesowy nie istnieje. Jest tylko baza danych i requesty HTTP. Klient klika „dodaj do koszyka" - system zapisuje wiersz w tabeli cart_items. Klika „złóż zamówienie" - system czyta z bazy, przetwarza, zapisuje z powrotem. Między kliknięciami nic się nie dzieje. System nie wie, że koszyk istnieje, dopóki ktoś o niego nie zapyta.

W Elixirze koszyk naprawdę istnieje. Jest procesem - kawałkiem kodu, który żyje w pamięci, pamięta swój stan, reaguje na zdarzenia i sam podejmuje decyzje (np. „minęło 30 minut bez aktywności - zwolnij zarezerwowane produkty"). To nie jest cache. To nie jest sesja. To GenServer.

Czym jest GenServer

GenServer (Generic Server) to abstrakcja nad procesem BEAM, która daje Ci:

  1. Stan - proces przechowuje dane w pamięci, między wywołaniami
  2. Interfejs - inne procesy komunikują się z nim przez wiadomości
  3. Gwarancje - wiadomości są przetwarzane po kolei, jedna po drugiej
  4. Nadzór - supervisor restartuje proces, jeśli coś pójdzie nie tak
defmodule MyApp.Counter do
  use GenServer

  # --- Interfejs publiczny (API) ---

  def start_link(initial_value) do
    GenServer.start_link(__MODULE__, initial_value)
  end

  def increment(pid) do
    GenServer.call(pid, :increment)
  end

  def get_value(pid) do
    GenServer.call(pid, :get_value)
  end

  # --- Implementacja (callbacks) ---

  @impl true
  def init(initial_value) do
    {:ok, initial_value}
  end

  @impl true
  def handle_call(:increment, _from, state) do
    new_state = state + 1
    {:reply, new_state, new_state}
  end

  @impl true
  def handle_call(:get_value, _from, state) do
    {:reply, state, state}
  end
end

# Użycie:
{:ok, pid} = MyApp.Counter.start_link(0)
MyApp.Counter.increment(pid)  #=> 1
MyApp.Counter.increment(pid)  #=> 2
MyApp.Counter.get_value(pid)  #=> 2

To wygląda na zwykły obiekt z metodami. Ale jest fundamentalna różnica: GenServer to osobny proces. Ma własną pamięć, własny garbage collector, własny stos wywołań. Crash tego procesu nie wpływa na żaden inny.

Dlaczego to zmienia architekturę

W klasycznym systemie webowym:

Request → Controller → Serwis → Baza → Serwis → Response
Request → Controller → Serwis → Baza → Serwis → Response
Request → Controller → Serwis → Baza → Serwis → Response

Każdy request: odczyt z bazy → obliczenia → zapis do bazy
Baza danych = jedyne źródło stanu
Baza danych = wąskie gardło

W systemie z GenServerami:

                    ┌──────────────┐
Request ──────────► │ GenServer    │ ──► Response
                    │ (stan w RAM) │
                    └──────────────┘

                    (zapis do bazy
                     asynchronicznie,
                     co N sekund
                     lub przy zamknięciu)

Stan jest w pamięci, nie w bazie. Odpowiedź jest natychmiastowa - nie czekasz na PostgreSQL. Baza danych służy do trwałości, nie do obsługi każdego kliknięcia.

Przykład 1: Koszyk zakupowy

Typowy koszyk w e-commerce - w tradycyjnym podejściu każda operacja to zapytanie do bazy:

Dodaj produkt:     INSERT INTO cart_items (...) → 3-8 ms
Zmień ilość:       UPDATE cart_items SET ... → 3-8 ms
Pobierz koszyk:    SELECT ... JOIN products JOIN prices → 5-15 ms
Oblicz sumę:       SELECT SUM(...) → 3-8 ms
Sprawdź promocje:  SELECT ... FROM promotions → 5-10 ms
────────────────────────────────────────────────
Jedno kliknięcie „dodaj do koszyka": 20-50 ms, 3-5 zapytań SQL

GenServer - koszyk jako żywy proces:

defmodule MyApp.Cart do
  use GenServer

  defstruct [:user_id, :items, :total, :discount, :updated_at]

  # --- API ---

  def start_link(user_id) do
    GenServer.start_link(__MODULE__, user_id, name: via(user_id))
  end

  def add_item(user_id, product_id, quantity \\ 1) do
    GenServer.call(via(user_id), {:add_item, product_id, quantity})
  end

  def remove_item(user_id, product_id) do
    GenServer.call(via(user_id), {:remove_item, product_id})
  end

  def get_summary(user_id) do
    GenServer.call(via(user_id), :get_summary)
  end

  def checkout(user_id) do
    GenServer.call(via(user_id), :checkout)
  end

  defp via(user_id), do: {:via, Registry, {MyApp.CartRegistry, user_id}}

  # --- Implementacja ---

  @impl true
  def init(user_id) do
    # Wczytaj koszyk z bazy (jeśli istnieje) przy starcie
    cart = load_from_db(user_id)
    # Ustaw timer - jeśli 30 min bez aktywności, zwolnij zasoby
    timer = Process.send_after(self(), :timeout, :timer.minutes(30))
    {:ok, %{cart: cart, timer: timer}}
  end

  @impl true
  def handle_call({:add_item, product_id, quantity}, _from, state) do
    product = Products.get_with_price!(product_id)

    cart =
      state.cart
      |> add_or_update_item(product, quantity)
      |> apply_promotions()
      |> recalculate_total()

    state = %{state | cart: cart} |> reset_timer()
    schedule_persist()

    {:reply, {:ok, cart_summary(cart)}, state}
  end

  @impl true
  def handle_call(:checkout, _from, state) do
    case Orders.create_from_cart(state.cart) do
      {:ok, order} ->
        {:stop, :normal, {:ok, order}, state}

      {:error, reason} ->
        {:reply, {:error, reason}, state}
    end
  end

  @impl true
  def handle_info(:persist, state) do
    persist_to_db(state.cart)
    {:noreply, state}
  end

  @impl true
  def handle_info(:timeout, state) do
    # 30 minut bez aktywności - zapisz i zakończ proces
    persist_to_db(state.cart)
    release_reserved_stock(state.cart)
    {:stop, :normal, state}
  end

  defp schedule_persist do
    # Zapisuj do bazy co 5 sekund, nie przy każdej operacji
    Process.send_after(self(), :persist, :timer.seconds(5))
  end

  defp reset_timer(state) do
    Process.cancel_timer(state.timer)
    timer = Process.send_after(self(), :timeout, :timer.minutes(30))
    %{state | timer: timer}
  end
end

Rezultat:

MetrykaTradycyjny (baza)GenServer (pamięć)
„Dodaj do koszyka"20-50 ms< 1 ms
Zapytania SQL per kliknięcie3-50
Obciążenie bazy (100 koszyków)300-500 zapytań/sek~20 zapytań/sek
Auto-cleanup nieaktywnychCron job co godzinęTimer per koszyk (dokładny)
Rezerwacja stockuRęczna (osobny system)Wbudowana w proces

100 aktywnych koszyków to 100 procesów BEAM - ~260 KB pamięci łącznie. Baza danych oddycha, użytkownik ma natychmiastowe odpowiedzi.

Przykład 2: Maszyna stanowa zamówienia

Zamówienie przechodzi przez stany: draft → confirmed → paid → shipped → completed. W tradycyjnym systemie to kolumna status w bazie i nadzieja, że nikt nie ustawi jej ręcznie na wartość, której nie powinien.

W GenServerze zamówienie wie, w jakim jest stanie, i samo pilnuje reguł:

defmodule MyApp.OrderProcess do
  use GenServer

  @transitions %{
    draft:     %{confirm: :confirmed, cancel: :cancelled},
    confirmed: %{pay: :paid, cancel: :cancelled},
    paid:      %{ship: :shipped, refund: :refunded},
    shipped:   %{deliver: :completed, return: :returned},
    completed: %{},
    cancelled: %{},
    refunded:  %{},
    returned:  %{}
  }

  # --- API ---

  def start_link(order_id) do
    GenServer.start_link(__MODULE__, order_id, name: via(order_id))
  end

  def confirm(order_id, confirmed_by) do
    GenServer.call(via(order_id), {:transition, :confirm, %{confirmed_by: confirmed_by}})
  end

  def pay(order_id, payment_ref) do
    GenServer.call(via(order_id), {:transition, :pay, %{payment_ref: payment_ref}})
  end

  def ship(order_id, tracking_number) do
    GenServer.call(via(order_id), {:transition, :ship, %{tracking_number: tracking_number}})
  end

  def get_status(order_id) do
    GenServer.call(via(order_id), :get_status)
  end

  def get_history(order_id) do
    GenServer.call(via(order_id), :get_history)
  end

  defp via(order_id), do: {:via, Registry, {MyApp.OrderRegistry, order_id}}

  # --- Implementacja ---

  @impl true
  def init(order_id) do
    order = Orders.get_order!(order_id)
    history = Orders.get_history(order_id)
    {:ok, %{order: order, status: order.status, history: history}}
  end

  @impl true
  def handle_call({:transition, action, metadata}, _from, state) do
    current = state.status
    transitions = Map.get(@transitions, current, %{})

    case Map.get(transitions, action) do
      nil ->
        {:reply, {:error, "nie można wykonać #{action} w stanie #{current}"}, state}

      next_status ->
        case execute_transition(state, action, next_status, metadata) do
          {:ok, new_state} ->
            {:reply, {:ok, next_status}, new_state}

          {:error, reason} ->
            {:reply, {:error, reason}, state}
        end
    end
  end

  @impl true
  def handle_call(:get_status, _from, state) do
    {:reply, state.status, state}
  end

  @impl true
  def handle_call(:get_history, _from, state) do
    {:reply, state.history, state}
  end

  defp execute_transition(state, action, next_status, metadata) do
    # Walidacja specyficzna dla przejścia
    with :ok <- validate_transition(state, action, metadata),
         {:ok, order} <- persist_transition(state.order, next_status, metadata),
         :ok <- emit_side_effects(action, order, metadata) do

      event = %{
        from: state.status,
        to: next_status,
        action: action,
        metadata: metadata,
        at: DateTime.utc_now()
      }

      {:ok, %{state |
        order: order,
        status: next_status,
        history: [event | state.history]
      }}
    end
  end

  defp validate_transition(state, :pay, %{payment_ref: ref}) do
    cond do
      is_nil(ref) -> {:error, "brak referencji płatności"}
      state.order.total_price |> Decimal.eq?(0) -> {:error, "zamówienie na 0 PLN"}
      true -> :ok
    end
  end

  defp validate_transition(state, :ship, %{tracking_number: tn}) do
    if is_nil(tn) or String.trim(tn) == "" do
      {:error, "brak numeru przesyłki"}
    else
      :ok
    end
  end

  defp validate_transition(_state, _action, _metadata), do: :ok

  defp emit_side_effects(:confirm, order, _meta) do
    Phoenix.PubSub.broadcast(MyApp.PubSub, "orders", {:order_confirmed, order})
    Notifications.send_confirmation(order)
    :ok
  end

  defp emit_side_effects(:ship, order, %{tracking_number: tn}) do
    Phoenix.PubSub.broadcast(MyApp.PubSub, "orders", {:order_shipped, order})
    Notifications.send_shipping_info(order, tn)
    :ok
  end

  defp emit_side_effects(_action, _order, _meta), do: :ok
end

Co daje ten model:

  • Niemożliwe przejścia są niemożliwe - nie da się wysłać nieopłaconego zamówienia
  • Historia jest w pamięci - pełna ścieżka audytowa bez dodatkowych zapytań
  • Side effects są automatyczne - potwierdzenie zamówienia = email + PubSub, zawsze
  • Walidacja per przejście - wysyłka wymaga tracking number, płatność wymaga referencji
  • Dashboard w czasie rzeczywistym - PubSub rozgłasza każdą zmianę do LiveView

Przykład 3: Rate limiter

Klasyczny problem: API partnera pozwala na 100 requestów na minutę. Jak pilnować limitu bez Redisa?

defmodule MyApp.RateLimiter do
  use GenServer

  def start_link(opts) do
    name = Keyword.fetch!(opts, :name)
    limit = Keyword.get(opts, :limit, 100)
    window_ms = Keyword.get(opts, :window_ms, 60_000)
    GenServer.start_link(__MODULE__, %{limit: limit, window_ms: window_ms}, name: name)
  end

  @doc "Sprawdź, czy można wykonać request. Zwraca :ok lub {:error, :rate_limited}"
  def check(name) do
    GenServer.call(name, :check)
  end

  @doc "Wykonaj funkcję z rate limitingiem"
  def execute(name, fun) do
    case check(name) do
      :ok -> fun.()
      {:error, :rate_limited} = err -> err
    end
  end

  # --- Implementacja ---

  @impl true
  def init(config) do
    {:ok, %{
      config: config,
      requests: :queue.new(),
      count: 0
    }}
  end

  @impl true
  def handle_call(:check, _from, state) do
    now = System.monotonic_time(:millisecond)
    state = purge_old_requests(state, now)

    if state.count < state.config.limit do
      requests = :queue.in(now, state.requests)
      {:reply, :ok, %{state | requests: requests, count: state.count + 1}}
    else
      {:reply, {:error, :rate_limited}, state}
    end
  end

  defp purge_old_requests(state, now) do
    cutoff = now - state.config.window_ms
    {requests, count} = drop_before(state.requests, state.count, cutoff)
    %{state | requests: requests, count: count}
  end

  defp drop_before(queue, count, cutoff) do
    case :queue.peek(queue) do
      {:value, timestamp} when timestamp < cutoff ->
        {_, queue} = :queue.out(queue)
        drop_before(queue, count - 1, cutoff)

      _ ->
        {queue, count}
    end
  end
end

# Użycie:
# W Application supervisor:
children = [
  {MyApp.RateLimiter, name: :api_partner_x, limit: 100, window_ms: 60_000},
  {MyApp.RateLimiter, name: :api_partner_y, limit: 50, window_ms: 60_000}
]

# W kodzie:
MyApp.RateLimiter.execute(:api_partner_x, fn ->
  PartnerAPI.send_order(order_data)
end)
# => {:ok, response} lub {:error, :rate_limited}

Zero Redisa. Zero zewnętrznych bibliotek. 50 linii kodu, działa natychmiast, precyzyjne sliding window.

Przykład 4: Cache z automatycznym odświeżaniem

Cennik produktów zmienia się raz na godzinę. Nie chcesz odpytywać bazy przy każdym requeście, ale potrzebujesz aktualnych danych:

defmodule MyApp.PriceCache do
  use GenServer

  @refresh_interval :timer.minutes(15)

  def start_link(_opts) do
    GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
  end

  def get_price(product_id) do
    GenServer.call(__MODULE__, {:get_price, product_id})
  end

  def get_all_prices do
    GenServer.call(__MODULE__, :get_all)
  end

  # --- Implementacja ---

  @impl true
  def init(:ok) do
    prices = load_all_prices()
    schedule_refresh()
    {:ok, %{prices: prices, loaded_at: DateTime.utc_now()}}
  end

  @impl true
  def handle_call({:get_price, product_id}, _from, state) do
    price = Map.get(state.prices, product_id)
    {:reply, price, state}
  end

  @impl true
  def handle_call(:get_all, _from, state) do
    {:reply, state.prices, state}
  end

  @impl true
  def handle_info(:refresh, _state) do
    prices = load_all_prices()
    schedule_refresh()
    {:noreply, %{prices: prices, loaded_at: DateTime.utc_now()}}
  end

  defp load_all_prices do
    Products.list_active_prices()
    |> Map.new(fn p -> {p.id, p.price} end)
  end

  defp schedule_refresh do
    Process.send_after(self(), :refresh, @refresh_interval)
  end
end

# 10 000 requestów/sekundę pyta o ceny
# 0 zapytań do bazy - wszystko z pamięci
# Dane odświeżane co 15 minut automatycznie
# Jeśli proces padnie - supervisor restartuje, init/1 ładuje z bazy

Przykład 5: Sesja użytkownika z timeout

W tradycyjnym systemie sesja to rekord w bazie lub wpis w Redis. W Elixirze sesja to żywy proces, który sam się kończy po czasie nieaktywności:

defmodule MyApp.UserSession do
  use GenServer

  @session_timeout :timer.minutes(30)

  def start_link(user_id) do
    GenServer.start_link(__MODULE__, user_id, name: via(user_id))
  end

  def touch(user_id) do
    GenServer.cast(via(user_id), :touch)
  end

  def get_activity(user_id) do
    GenServer.call(via(user_id), :get_activity)
  end

  def active_sessions_count do
    Registry.count(MyApp.SessionRegistry)
  end

  defp via(user_id), do: {:via, Registry, {MyApp.SessionRegistry, user_id}}

  @impl true
  def init(user_id) do
    state = %{
      user_id: user_id,
      started_at: DateTime.utc_now(),
      last_activity: DateTime.utc_now(),
      page_views: 0
    }

    timer = Process.send_after(self(), :session_timeout, @session_timeout)
    Phoenix.PubSub.broadcast(MyApp.PubSub, "sessions", {:session_started, user_id})
    {:ok, Map.put(state, :timer, timer)}
  end

  @impl true
  def handle_cast(:touch, state) do
    Process.cancel_timer(state.timer)
    timer = Process.send_after(self(), :session_timeout, @session_timeout)

    {:noreply, %{state |
      last_activity: DateTime.utc_now(),
      page_views: state.page_views + 1,
      timer: timer
    }}
  end

  @impl true
  def handle_call(:get_activity, _from, state) do
    {:reply, Map.delete(state, :timer), state}
  end

  @impl true
  def handle_info(:session_timeout, state) do
    Phoenix.PubSub.broadcast(MyApp.PubSub, "sessions", {:session_ended, state.user_id})
    AuditLog.log_session(state)
    {:stop, :normal, state}
  end
end

# Ile aktywnych sesji? Jedno wywołanie, zero bazy:
MyApp.UserSession.active_sessions_count()
#=> 847

# Dashboard admina widzi w czasie rzeczywistym:
# - Kto jest zalogowany
# - Ile wyświetleń stron per sesja
# - Kiedy ostatnia aktywność
# Wszystko bez odpytywania bazy

GenServer vs tradycyjne podejścia

ProblemTradycyjnieGenServer
Koszyk zakupowyTabela carts + cart_items, 3-5 zapytań per klikProces w pamięci, < 1 ms, zapis do bazy co N sekund
Rate limitingRedis + TTL + Lua script50 linii kodu, zero zależności
Cache cennikaRedis + cron odświeżającyProces z Process.send_after, auto-refresh
Sesja użytkownikaRedis/Memcached + TTLProces z timerem, auto-cleanup
Maszyna stanowaKolumna status + walidacja w serwisieProces z @transitions, niemożliwe złe przejścia
Kolejka zadańRabbitMQ / Redis + SidekiqOban (PostgreSQL) lub GenServer
Lock/mutexSELECT FOR UPDATE lub Redis SETNXMailbox GenServera = naturalny mutex

Mailbox = naturalny mutex

To jest subtelna, ale potężna cecha GenServera. Każdy proces BEAM ma mailbox - kolejkę wiadomości przetwarzanych jedna po drugiej. To eliminuje cały zestaw problemów współbieżności:

# Problem: dwóch handlowców jednocześnie rezerwuje ostatnią sztukę

# Tradycyjnie (bez locka):
# Handlowiec A: SELECT stock FROM products WHERE id = 1  → stock = 1
# Handlowiec B: SELECT stock FROM products WHERE id = 1  → stock = 1
# Handlowiec A: UPDATE products SET stock = 0  → OK
# Handlowiec B: UPDATE products SET stock = -1 → BUG! Stock ujemny!

# Z GenServerem:
defmodule MyApp.StockManager do
  use GenServer

  def reserve(product_id, quantity) do
    GenServer.call(via(product_id), {:reserve, quantity})
  end

  @impl true
  def handle_call({:reserve, quantity}, _from, stock) do
    if stock >= quantity do
      {:reply, {:ok, stock - quantity}, stock - quantity}
    else
      {:reply, {:error, :insufficient_stock}, stock}
    end
  end
end

# Handlowiec A: reserve(1, 1) → wiadomość w mailboxie
# Handlowiec B: reserve(1, 1) → wiadomość CZEKA w kolejce
# GenServer przetwarza A: stock = 1, quantity = 1 → {:ok, 0}
# GenServer przetwarza B: stock = 0, quantity = 1 → {:error, :insufficient_stock}
#
# Zero locków, zero race conditions, zero SELECT FOR UPDATE
# Mailbox = sekwencyjne przetwarzanie = naturalny mutex

Jak GenServery współpracują

Prawdziwy system to nie jeden GenServer - to sieć procesów, które komunikują się przez wiadomości:

Diagram architektury: sieć procesów GenServer pod Supervisorem — CartProcess, OrderProcess, RateLimiter, Payment Gateway, PubSub broadcast do LiveView i Notification

Każdy prostokąt to osobny proces. Każdy ma swój stan, swoje reguły, swój lifecycle. Jeśli NotificationSender padnie - zamówienie nadal przejdzie. Supervisor zrestartuje wysyłkę powiadomień, a wiadomość zostanie ponowiona.

Registry - znajdź proces po nazwie biznesowej

Jak znaleźć GenServer koszyka dla użytkownika 42, jeśli w systemie jest 500 aktywnych koszyków?

# Registry pozwala adresować procesy po dowolnym kluczu

# Konfiguracja w Application supervisor:
children = [
  {Registry, keys: :unique, name: MyApp.CartRegistry},
  {Registry, keys: :unique, name: MyApp.OrderRegistry},
  {Registry, keys: :unique, name: MyApp.SessionRegistry},
  {DynamicSupervisor, name: MyApp.CartSupervisor, strategy: :one_for_one}
]

# Uruchomienie koszyka dla użytkownika:
def ensure_cart_started(user_id) do
  case Registry.lookup(MyApp.CartRegistry, user_id) do
    [{pid, _}] ->
      {:ok, pid}

    [] ->
      DynamicSupervisor.start_child(
        MyApp.CartSupervisor,
        {MyApp.Cart, user_id}
      )
  end
end

# Teraz:
MyApp.Cart.add_item(user_id, product_id, 2)
# Registry automatycznie znajdzie proces koszyka tego użytkownika
# Bez bazy, bez Redisa, bez wyszukiwania - O(1) lookup

Kiedy używać GenServera, a kiedy bazy

GenServer nie zastępuje bazy danych. To inna warstwa - warstwa procesów biznesowych żyjących w pamięci:

SytuacjaUżyj GenServeraUżyj bazy danych
Dane muszą przetrwać restart serwera
Potrzebujesz natychmiastowej odpowiedzi
Dane muszą być dostępne z wielu serwisów
Obiekt ma timeout / lifecycle
Potrzebujesz naturalnego mutexa
Potrzebujesz raportów i analityki
Stan jest tymczasowy (sesja, koszyk)
Stan jest trwały (zamówienie, faktura)✓ + baza

Najlepsze podejście: GenServer + baza razem. GenServer jako warstwa szybkiego dostępu i logiki, PostgreSQL jako warstwa trwałości. GenServer zapisuje do bazy asynchronicznie, co N sekund lub przy ważnych zdarzeniach. Przy restarcie procesu - odtwarza stan z bazy.

Ile to kosztuje (pamięć)

# Jeden proces BEAM:
:erlang.process_info(self(), :memory)
#=> {:memory, 2688}  - 2.6 KB

# Koszyk z 10 produktami: ~4-5 KB
# Sesja użytkownika: ~3 KB
# Rate limiter (100 wpisów): ~5 KB
# Maszyna stanowa zamówienia: ~4 KB

# 1000 aktywnych koszyków = ~5 MB
# 5000 sesji użytkowników = ~15 MB
# 100 rate limiterów = ~0.5 MB
# ─────────────────────────────
# Razem: ~20 MB

# Serwer z 32 GB RAM może obsłużyć
# MILIONY prostych procesów jednocześnie

Dla porównania: Redis z 1000 koszykami to osobny serwer, osobne utrzymanie, osobna awaria, serializacja/deserializacja przy każdym dostępie, opóźnienie sieciowe. GenServer to zero dodatkowej infrastruktury.

Co to oznacza dla biznesu

System tradycyjny: baza danych jest centrum wszystkiego. Każde kliknięcie = zapytanie SQL. 50 użytkowników jednocześnie = 200 zapytań/sekundę. Baza staje się wąskim gardłem. Rozwiązanie? Większy serwer. Cache w Redis. CDN. Każda warstwa to dodatkowy koszt i złożoność.

System z GenServerami: procesy biznesowe żyją w pamięci. Koszyk odpowiada w < 1 ms. Maszyna stanowa pilnuje reguł sama. Rate limiter działa bez Redisa. Baza danych obsługuje tylko zapisy - 10× mniej obciążona. Jeden serwer zamiast trzech.

MetrykaTradycyjny stackElixir + GenServer
Czas odpowiedzi (koszyk)20-50 ms< 1 ms
Zapytania SQL per użytkownik/min30-602-5
Zewnętrzna infrastrukturaRedis + bazaTylko baza
Procesy z własnym lifecycleBrak (cron joby)Natywne
Obsługa 1000 użytkowników online3+ serwery1 serwer

GenServer to nie jest „fajny feature Elixira". To sposób myślenia o architekturze, w którym procesy biznesowe są prawdziwymi obiektami w systemie - ze stanem, zachowaniem i cyklem życia. Zamówienie nie jest wierszem w tabeli. Jest żywym procesem, który wie, co wolno, a czego nie, i sam reaguje na zdarzenia.


Chcesz zobaczyć, jak GenServery mogą uprościć architekturę Twojego systemu? Porozmawiajmy - pokażemy model dopasowany do Twoich procesów biznesowych.