- GigaElixir Gazette
- Posts
- 🎮 Your GenServers Got Too Big Because You Mixed Everything Together
🎮 Your GenServers Got Too Big Because You Mixed Everything Together
One developer's World of Warcraft server refactor reveals why separating data from orchestration changes everything
Welcome to GigaElixir Gazette, your 5-minute digest of Elixir ecosystem news that actually matters 👋.
. WEEKLY PICKS .
🤖 Arcana Brings Production-Grade RAG to Phoenix Without External Vector Databases: Why run ChromaDB or Pinecone when your Ecto repo can handle vector search? New embeddable library adds AI question answering using your existing database. Both simple RAG and agentic pipelines work out of the box. Swap vector stores (pgvector for production, HNSWLib for testing), chunking strategies, and embedding providers without changing code. Optional GraphRAG combines vector search with knowledge graph traversal. The architectural win is pluggable everything.
💰 Two Simultaneous Withdrawals. Both Approved. Double-Spend: PostgreSQL with application-level balance checks looked solid until production exposed the race condition. iGaming platform moved to TigerBeetle after users figured out how to exploit it. TigerBeetle enforces constraints at database layer with debits_must_not_exceed_credits flag. Every transfer is immutable. Pre-defined double-entry schema prevents design mistakes. The tutorial walks through the full migration. Where PostgreSQL broke, how TigerBeetle fixed it, the Elixir code that made it work.
📝 Turns Out Struct Updates Weren't Actually Type-Safe: Upgrade to Elixir 1.19 and suddenly %Product{product | variants: filtered} throws compiler warnings. The struct update syntax implied runtime type checking that didn't exist. Fix requires pattern matching: {:ok, %Product{} = product} gives the compiler evidence for type inference. This enables Elixir's gradual typing system. The verbosity is intentional. When someone refactors that function later, the compiler catches it instead of production.
🚀 Credo Just Got 3.6x Faster and You Don't Have to Change Anything: New caching strategy and optimized AST traversal drop runtime from 33 to 9 seconds on large codebases. One team saw CI pipeline time cut 75% on a 200+ file Phoenix monolith. Backward compatible. Just upgrade and get the speedup. New architecture builds a single traversal that all rules share. The kind of performance win that makes you wonder why you tolerated the old version.
🎯 Phoenix 1.8 Ships with AGENTS.md So LLMs Stop Making the Same Mistakes: New apps generated with phx.new now include AGENTS.md containing guidelines extracted from the Phoenix.new agent. The file corrects common mistakes frontier models make with Elixir syntax, idioms, and critical API usage. Models stop generating deprecated patterns, use proper OTP idioms, handle Ecto queries correctly. It's the difference between LLMs that "kind of" know Phoenix and ones that write production code correctly. daisyUI ships for theming, scopes handle secure data access, and magic links work out of the box.

Your GenServers Got Out of Hand Because You're Orchestrating Instead of Modeling
Every now and then you hit a wall that makes you realize you've been solving the wrong problem. For the Thistle Tea World of Warcraft server project, that wall was implementing mob chase mechanics.
The mob GenServer file hit 500+ lines. Network packet parsing sat next to movement calculations sat next to combat rules. Testing meant spinning up supervision trees instead of calling functions. When mob combat needed to interrupt movement and trigger chase behavior, the state management became impossible to reason about.
Deployments became anxiety-inducing. Roll back a bad release? Hope the GenServer state didn't drift. Debug a production issue? Attach observer and try to make sense of nested map structures holding movement state, combat flags, and network buffers all mixed together. Performance problems? Good luck profiling when game logic, network I/O, and state management all compete in the same process.
Tried ECS. Entity component systems make sense in theory but require polling. BEAM's actor model is reactive. Full rewrite seemed tempting but so much already worked.
The breakthrough came from "Designing Elixir Systems with OTP." Stop thinking about GenServers as "things that hold game state and logic." Start thinking about them as "thin wrappers around pure functions."
A mob became %Mob{object: %Object{}, unit: %Unit{}} instead of GenServer state. Functions like take_damage/2 became pure. Take an entity struct, return updated entity struct. The GenServer just calls the function and stores the result.
The magic happened when building entities from components. Before: separate Player.take_damage/2 and Mob.take_damage/2 functions. After: single function that pattern matches on %{unit: %Unit{health: health}}. Works on players, mobs, game objects.
Message abstractions eliminated the other complexity. Instead of raw binary packets, messages became structs: %SmsgDestroyObject{guid: "1234"} |> Network.send_packet(). The connection handler shrinks to just routing.
Tests became trivial. Want to test that taking damage reduces health to zero? Call the function with test data and assert. No supervision trees, no message passing, no async timing issues.
The boundary layer stayed flexible. Right now it's process-per-entity. If that doesn't work later, group entities by map cell. Only the boundary layer changes.
Your GenServers probably look like this: hundreds of lines mixing I/O with business logic. That's where the code naturally grows when you think "GenServer holds state and logic."
The fix isn't abandoning OTP. It's remembering what OTP is for: managing process lifecycle, handling failures, coordinating operations. Not implementing business rules.
Remember, for managing complex application state:
Separate data modeling from process orchestration – Pure structs and functions for business logic, GenServers only for process lifecycle.
Build entities from reusable components – Composing structs eliminates entity-specific implementations. One function works everywhere.
Message abstractions reduce cognitive overhead – Structs for protocol messages instead of raw binary manipulation.
Boundary layers adapt to scale requirements – Functional core stays stable while process architecture changes.
. TIRED OF DEVOPS HEADACHES? .
Deploy your next Elixir app hassle-free with Gigalixir and focus more on coding, less on ops.
We're specifically designed to support all the features that make Elixir special, so you can keep building amazing things without becoming a DevOps expert.
See you next week,
Michael
P.S. Forward this to a friend who loves Elixir as much as you do 💜