home

Elixir Cycles

This post has a very limited audience. If you love Elixir it will make you angry, so probably you shouldn’t read it. If you don’t care for Elixir then probably it’s irrelevant for you as well

I’m - however somewhat tired of making same argument on the Internet again and again and decided do crystalize it into an easier to share form.

Disclaimer: I worked with Elixir on a level most people don’t usually work and I found things that made me not like it anymore.

Elixir has problem with circular dependencies

Probably better show than tell. Checked on latest stable of 04.06.2026:

$ elixir --version
Erlang/OTP 29 [erts-17.0.1] [source] [64-bit] [smp:12:12] [ds:12:12:10] [async-threads:1] [jit]

Elixir 1.20.0 (compiled with Erlang/OTP 29)

New, bootstrapped Phoenix application

$ mix phx.new example && cd example/

Checking for cycles:

$ mix xref graph --format cycles
1 cycles found. Showing them in decreasing size:

Cycle of length 4:

    lib/example_web/components/layouts.ex
    lib/example_web/controllers/page_controller.ex
    lib/example_web/endpoint.ex
    lib/example_web/router.ex
  • router.ex:8 - plug :put_root_layout, html: {ExampleWeb.Layouts, :root} makes Router -> Layouts.
  • router.ex:20 - get "/", PageController, :home makes Router -> PageController.
  • endpoint.ex:54 - plug ExampleWeb.Router makes Endpoint -> Router.
  • layouts.ex:6 - use ExampleWeb, :html expands Phoenix.VerifiedRoutes, making Layouts -> Endpoint and Layouts -> Router.
  • page_controller.ex:2 - use ExampleWeb, :controller also expands Phoenix.VerifiedRoutes, making PageController -> Endpoint and PageController -> Router.
  • example_web.ex:101-104 - use Phoenix.VerifiedRoutes, endpoint: ExampleWeb.Endpoint, router: ExampleWeb.Router, statics: ExampleWeb.static_paths() is the shared macro code that creates those back-references.

Unstable builds

Full clean builds recipe (remove everything, get everything, compile everything, compare MD5 of compiled files):

$ for i in 1 2 3 4; do mix clean && mix deps.clean --all && mix deps.get && mix compile; (fd -uu beam | xargs -L1 md5) | sort > ${i}.md5; done

Diff comparison (numbers have to be divided by 2, as MD5 shows left+right):

$ for x in 1 2 3 4; for y in 1 2 3 4; \
    do echo -n "$x.md5 vs $y.md5: "; \
    diff $x.md5 $y.md5 | rg 'MD5 ' | wc -l; \
  done

1.md5 vs 1.md5:        0
1.md5 vs 2.md5:       62
1.md5 vs 3.md5:       62
1.md5 vs 4.md5:       58
2.md5 vs 1.md5:       62
2.md5 vs 2.md5:        0
2.md5 vs 3.md5:       62
2.md5 vs 4.md5:       54
3.md5 vs 1.md5:       62
3.md5 vs 2.md5:       62
3.md5 vs 3.md5:        0
3.md5 vs 4.md5:       52
4.md5 vs 1.md5:       58
4.md5 vs 2.md5:       54
4.md5 vs 3.md5:       52
4.md5 vs 4.md5:        0

Excluding Elixir keyword (used for Elixir applications and libraries):

$ for x in 1 2 3 4; for y in 1 2 3 4; do \
    echo -n "$x.md5 vs $y.md5: "; \
    diff $x.md5 $y.md5 | rg 'MD5 ' | rg -v 'Elixir' | wc -l; \
  done

1.md5 vs 1.md5:        0
1.md5 vs 2.md5:        0
1.md5 vs 3.md5:        0
1.md5 vs 4.md5:        0
2.md5 vs 1.md5:        0
2.md5 vs 2.md5:        0
2.md5 vs 3.md5:        0
2.md5 vs 4.md5:        0
3.md5 vs 1.md5:        0
3.md5 vs 2.md5:        0
3.md5 vs 3.md5:        0
3.md5 vs 4.md5:        0
4.md5 vs 1.md5:        0
4.md5 vs 2.md5:        0
4.md5 vs 3.md5:        0
4.md5 vs 4.md5:        0

Changed hash list

Not a lot of value unless you’re curious. Some “core” libraries are present though.

$ diff 1.md5 3.md5 | rg '> MD5

> MD5 (_build/dev/lib/bandit/ebin/Elixir.Bandit.beam) = 52f670a505177444a145d78334fe2349
> MD5 (_build/dev/lib/bandit/ebin/Elixir.Bandit.WebSocket.Frame.beam) = 4da3bef15d354c0f2ff667fd0b42a155
> MD5 (_build/dev/lib/db_connection/ebin/Elixir.DBConnection.Ownership.Manager.beam) = 082cfc67c9545223e2e5c2fc3adad7da
> MD5 (_build/dev/lib/ecto_sql/ebin/Elixir.Ecto.Adapters.SQL.beam) = af7be2f9fc39116dd259880aafbcd65c
> MD5 (_build/dev/lib/ecto_sql/ebin/Elixir.Ecto.Migration.SchemaMigration.beam) = 914f58411f54a59ef749dd2d0afcb28d
> MD5 (_build/dev/lib/ecto/ebin/Elixir.Ecto.Query.beam) = 9548b7ff95389e724db3130bbd9732f3
> MD5 (_build/dev/lib/example/consolidated/Elixir.Bandit.WebSocket.Frame.Serializable.beam) = 9cdc0525996655a9c05e8395311dfc0e
> MD5 (_build/dev/lib/example/consolidated/Elixir.Inspect.beam) = 84458295161be1d9b785acc3a87afef6
> MD5 (_build/dev/lib/example/ebin/Elixir.ExampleWeb.CoreComponents.beam) = 7031635cc1fd2e2c046fec92536ecc1e
> MD5 (_build/dev/lib/phoenix_live_dashboard/ebin/Elixir.Phoenix.LiveDashboard.AppInfoComponent.beam) = afa7738ef12c72f5c023c262163ccdcb
> MD5 (_build/dev/lib/phoenix_live_dashboard/ebin/Elixir.Phoenix.LiveDashboard.ChartComponent.beam) = d5a53fd98daa32730a9be1e5a7891620
> MD5 (_build/dev/lib/phoenix_live_dashboard/ebin/Elixir.Phoenix.LiveDashboard.LayeredGraphComponent.beam) = f707a280e4669040efba28df366197a4
> MD5 (_build/dev/lib/phoenix_live_dashboard/ebin/Elixir.Phoenix.LiveDashboard.LayoutView.beam) = e0b0969cdd2b5a51876bd5060846dc67
> MD5 (_build/dev/lib/phoenix_live_dashboard/ebin/Elixir.Phoenix.LiveDashboard.MetricsPage.beam) = 784381366f4880684217a907c9a948e9
> MD5 (_build/dev/lib/phoenix_live_dashboard/ebin/Elixir.Phoenix.LiveDashboard.NavBarComponent.beam) = 7fc42112e34e3d7b9010d59e6bd356f8
> MD5 (_build/dev/lib/phoenix_live_dashboard/ebin/Elixir.Phoenix.LiveDashboard.PageBuilder.beam) = 56c9cfca67ebedbf37b0cf55384b914e
> MD5 (_build/dev/lib/phoenix_live_dashboard/ebin/Elixir.Phoenix.LiveDashboard.PageLive.beam) = cb0b78a30a7b0fee80544df822adba8b
> MD5 (_build/dev/lib/phoenix_live_dashboard/ebin/Elixir.Phoenix.LiveDashboard.PortInfoComponent.beam) = 5713291065fd59bb4116f95d0cdb1015
> MD5 (_build/dev/lib/phoenix_live_dashboard/ebin/Elixir.Phoenix.LiveDashboard.ProcessInfoComponent.beam) = cd23192855eb4e35629313b197cb4092
> MD5 (_build/dev/lib/phoenix_live_dashboard/ebin/Elixir.Phoenix.LiveDashboard.RequestLoggerPage.beam) = def62875df42ea25c67a72009dc920cd
> MD5 (_build/dev/lib/phoenix_live_dashboard/ebin/Elixir.Phoenix.LiveDashboard.TableComponent.beam) = be8a64e8be469ae6fa67a5b5a77f600b
> MD5 (_build/dev/lib/phoenix_live_view/ebin/Elixir.Phoenix.Component.beam) = 7b9dbeb9ce71e61c87442e223c5a999b
> MD5 (_build/dev/lib/phoenix_live_view/ebin/Elixir.Phoenix.LiveView.beam) = 967670d176a88c5a28a486d985ac93d2
> MD5 (_build/dev/lib/phoenix/ebin/Elixir.Phoenix.ConnTest.beam) = b971d00611f9894e925c95ba45daeafe
> MD5 (_build/dev/lib/plug/ebin/Elixir.Plug.Conn.Status.beam) = 9697007628c8bd539d499ed64c15b265
> MD5 (_build/dev/lib/postgrex/ebin/Elixir.DBConnection.Query.Postgrex.Query.beam) = a84043fc872721f600cd2e83c09a2970
> MD5 (_build/dev/lib/postgrex/ebin/Elixir.DBConnection.Query.Postgrex.TextQuery.beam) = bf566145d6d7495c9baef0cafa86d38c
> MD5 (_build/dev/lib/postgrex/ebin/Elixir.Postgrex.DefaultTypes.beam) = 0034dae15b84465c7ddacbc304d6a218
> MD5 (_build/dev/lib/postgrex/ebin/Elixir.Postgrex.ErrorCode.beam) = f53acd5b6220a1d0e5112b4a6da7343b
> MD5 (_build/dev/lib/postgrex/ebin/Elixir.Postgrex.Messages.beam) = 0d4bbe8ca602a37fff7a6ebda6db9dd5
> MD5 (_build/dev/lib/postgrex/ebin/Elixir.Postgrex.Utils.beam) = 7f4002327482ddbaaf5aee4a987355b3

Consequences

  • 98% of problems:
    • Impact on build time (few seconds at most on small-medium sized projects)
    • Impact on tooling (dialyzer was unusable when many cycles are present, optimization by removing cycles made elixir-ls runtime from 40s+ to <5s+)
  • remaining 2%
    • “Dirty build syndrome”, e.g. error about ecto not found where it’s obviously in deps - it happens when library X depended on ecto but ecto was recompiled AFTER library X was (my guess: it’s probably about hashes)
    • CI/release failures difficult to reproduce on dev env (because compilation order matters, and some dependencies could be dev only…)

Fixes

  • Roll out your own build system that stabilizes dependency (it’s easier than it sounds - create dependency graph based on missing dep errors during compilation - this approach also significantly speeds up build time)
  • Mind and actively prevent circular dependencies (add mix xref graph --format cycles 2>&1 | head -1 | cut -w -f1 as a CI param )
  • (Risky) Use runtime dependencies through dynamic resolution

Unrelated rant

Deprecation being an compile time error with --warnings-as-errors is terrible decision as it prevents gradual library replacement.

Przemysław Alexander Kamiński
vel xlii vel exlee

cb | gl | gh | li | rss

Powered by hugo and hugo-theme-nostyleplease.