Variant Systems

Elixir & Phoenix Vibe Code Cleanup

AI doesn't understand the BEAM. Your GenServers are bottlenecks and your supervision trees don't exist. We'll fix that.

At Variant Systems, we pair the right technology with the right approach to ship products that work.

Why this combination

  • AI wraps stateless logic in GenServers, creating unnecessary serialization points
  • LiveView components grow to 500+ lines with mixed concerns
  • Ecto changesets get bypassed with raw inserts and unsafe casts
  • Supervision trees are flat or missing, so crashes cascade

How LLMs Misunderstand the BEAM Runtime

AI treats Elixir like it treats every other language. That’s the fundamental problem. It generates code that runs, but ignores everything that makes Elixir powerful.

GenServers are the worst offender. AI creates a GenServer for everything. Fetching config? GenServer. Formatting a string? GenServer. Calling an external API? GenServer. Each unnecessary GenServer is a serialization point. Requests that could run concurrently now queue in a single process mailbox. Your concurrent language becomes sequential because AI doesn’t understand when to use a process and when to use a plain function.

LiveView components turn into monoliths. AI generates a single LiveView module that handles the socket connection, all event handlers, all assigns, and all template rendering in one file. A 600-line LiveView that manages a table, a form, a modal, and three different loading states. Nobody can modify one part without risking the others.

Ecto gets abused. AI skips changesets and uses Repo.insert_all with raw maps. It casts every field with cast/3 instead of being explicit about what’s allowed. Validations live in the controller instead of the schema. Your database accepts data your domain model says is invalid because the validation layer is in the wrong place.

Supervision trees are either flat or missing. AI creates all processes under the application supervisor with :one_for_one strategy. No hierarchy. No isolation. A crash in your email sender restarts your cache warmer. Nothing is grouped by failure domain.

Stripping Unnecessary GenServers and Rebuilding Supervision Trees

We start with :observer and :recon to understand the actual process landscape. How many processes exist? Which ones have growing mailboxes? Which handle the most messages? This gives us a real map of your system, not the one AI imagined.

Every GenServer gets evaluated. Does it hold state? Does it need to serialize access? If the answer to both is no, it becomes a plain module with functions. We’ve seen apps where removing 15 unnecessary GenServers cut average response time by 40%. The BEAM is fast at concurrency. It’s not magic at serialization.

LiveView modules get decomposed. We extract function components for presentation logic. Live components handle independent interactive sections. The parent LiveView orchestrates state and delegates rendering. A 600-line file becomes a 150-line orchestrator with four focused components that can be tested and modified independently.

Ecto schemas get proper changesets. Every field has explicit casting. Validations run in the changeset, not the controller. Constraints are backed by database-level enforcement. We add multi-clause changeset functions for different operations - create_changeset/2, update_changeset/2, admin_changeset/2 - so each operation only accepts the fields it should.

23 GenServers Become 5 That Actually Need State

Before: 23 GenServers, 18 of which don’t need to be processes. A LiveView that takes 200ms to handle a simple click event because it re-computes everything in the assigns. Ecto schemas with cast(attrs, __schema__(:fields)) that accept any field, including ones that should be internal.

After: 5 GenServers that genuinely manage state and concurrency. LiveView interactions under 10ms because components only update what changed. Ecto changesets that reject invalid data at the boundary. A supervision tree with three levels that isolates failures by domain.

The difference shows up in your error logs. Before cleanup, you see intermittent timeouts from GenServer calls hitting the 5-second default. After, those disappear entirely because the processes that were bottlenecks no longer exist.

Credo Rules and Boundary Checks That Enforce OTP Discipline

Credo gets configured with custom checks that flag common AI patterns. A GenServer without a corresponding supervisor? Warning. A LiveView over 200 lines? Warning. A changeset that casts all fields? Error. These checks run in CI on every pull request.

Boundary library enforces context separation at compile time. If AI generates code in the Accounts context that calls Billing internals directly, the build fails. Allowed dependencies are explicit. Cross-context calls go through public APIs.

Dialyzer typespecs cover all public functions. AI rarely adds specs, so the first run surfaces a wave of type mismatches. Once clean, the specs prevent future AI-generated code from passing the wrong types silently. We configure --halt-exit-status so Dialyzer failures block deployment.

We document OTP decisions. When to use a GenServer. When to use Task.Supervisor for fire-and-forget work. When to use Agent for simple shared state. These guidelines are specific to your system, not generic Elixir advice. Your team and your AI tools produce better code when the patterns are documented.

What you get

OTP architecture audit with process map and bottleneck analysis
GenServer rationalization - remove unnecessary processes
LiveView decomposition into focused components
Ecto schema and changeset cleanup with proper validations
Supervision tree design with proper restart strategies

Ideal for

  • Elixir apps built with AI that don't use OTP patterns correctly
  • Systems where GenServer mailboxes are growing and response times are degrading
  • LiveView apps that are slow because components are monolithic
  • Teams that need their Elixir codebase ready for production traffic

Other technologies

Industries

Ready to build?

Tell us about your project and we'll figure out how we can help.

Get in touch