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}makesRouter -> Layouts.router.ex:20-get "/", PageController, :homemakesRouter -> PageController.endpoint.ex:54-plug ExampleWeb.RoutermakesEndpoint -> Router.layouts.ex:6-use ExampleWeb, :htmlexpandsPhoenix.VerifiedRoutes, makingLayouts -> EndpointandLayouts -> Router.page_controller.ex:2-use ExampleWeb, :controlleralso expandsPhoenix.VerifiedRoutes, makingPageController -> EndpointandPageController -> 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 (
dialyzerwas unusable when many cycles are present, optimization by removing cycles madeelixir-lsruntime from 40s+ to <5s+)
- remaining 2%
- “Dirty build syndrome”, e.g. error about
ectonot found where it’s obviously in deps - it happens when libraryXdepended onectobutectowas recompiled AFTER libraryXwas (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…)
- “Dirty build syndrome”, e.g. error about
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 -f1as 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
Powered by hugo and hugo-theme-nostyleplease.