Boundary turns layering rules into compiler errors in an Elixir project
Manage and restrain cross-module dependencies in Elixir projects
At a glance
- What is it?
- Boundary is an MIT licensed Elixir library that makes cross module dependencies a compile time concern. You declare boundaries as named groups of modules, say which modules each one exports and which other boundaries it may depend on, and a mix compiler reports every call that breaks the rules, in the editor as well as in the terminal. It can also police calls into other applications, so Phoenix, Ecto and Mix can be confined to the layers allowed to use them.
- Who is it for?
- Boundary suits an Elixir team that has decided on a layered architecture and wants the layering to hold under pressure, particularly one that has been bitten by a context module reaching into a controller. It does not suit a small application with two modules, and it is the wrong tool if the rule you want to express is a runtime one rather than a compile time one.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 36 days ago.
- What is it written in?
- Mainly Elixir, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The rule is enforced where the mistake is made
Boundary is a library for managing and restraining cross module dependencies in Elixir projects, and the list of things it can prevent is the clearest statement of its purpose.
It can prevent invocations from the context layer to the web layer, prevent invocations from the web layer to internal context modules, prevent the usage of Phoenix and Plug in the context layer, limit Ecto usage in the web layer to only Ecto.Changeset, and allow Mix modules to be used only at compile time.
Every one of those is a rule about which layer may depend on which, and the important detail is where the rule is checked. Boundary is implemented as a mix compiler, so the validation happens during compilation, not in a test and not in a review. A developer writing a call that breaks the layering gets a compiler warning at the line that breaks it, which is the only point where changing it is cheap.
That is the difference from the alternatives a team already has. A code review can catch a layering violation if the reviewer notices it, and a test that asserts no module depends on another can catch it later, but neither of them points at the offending call while the author is still in the file.
The audience is a team with a layered architecture and the discipline to want it enforced, and the tool's approach is to make the architecture a property of the build rather than a habit.
Boundaries are named module groups with an export list
The core concept is small and worth stating precisely, because everything else follows from it. A boundary is a named group of one or more modules. Each boundary exports some, but not all, of its modules, and can depend on other boundaries.
Membership is derived rather than declared. Boundary modules are determined automatically from the boundary name, so the MySystem boundary contains the MySystem module and any module whose name starts with MySystem., such as MySystem.User and MySystem.User.Schema.
That convention is the whole ergonomics. A developer puts a module in a boundary by naming it consistently, which is the same convention Elixir already pushes teams towards with namespace prefixes, and the tool reads the existing structure instead of asking you to maintain a second list of file paths.
The configuration is a use of the Boundary macro in the boundary's own module, with deps and exports lists. In the README's example, which follows a Phoenix project generated with mix phx.new, MySystemWeb declares deps: [MySystem] and exports: [Endpoint], and MySystem.Application declares top_level?: true with deps: [MySystem, MySystemWeb].
The top_level? flag matters and is not explained further in the README. It marks a boundary as an entry point rather than a layer, which is consistent with the application boundary being allowed to reach both layers below it while those layers cannot reach each other upward.
What a violation looks like
The example violation is a context module calling into the web layer, which is the most common layering mistake in a Phoenix application.
The offending code is a MySystem.User module whose auth function calls MySystemWeb.Endpoint.url(), and the compiler response is a warning naming the boundary and the location:
$ mix compile
warning: forbidden reference to MySystemWeb
(references from MySystem to MySystemWeb are not allowed)
lib/my_system/user.ex:3Two details make that message useful. It names the boundary that was referenced rather than just the module, so the fix is a design question and not a search for the right function. And it gives the file and line, so the warning lands in the editor rather than only in a build log.
The rules in the example configuration are that modules in the MySystemWeb boundary may invoke functions from modules exported by the MySystem boundary, that modules in the MySystem.Application namespace may invoke functions from modules exported by MySystem and MySystemWeb, and that all other cross boundary calls are not permitted.
The last clause is the important one. A dependency list that only says what is allowed is not a constraint, and this one is a whitelist, so a new cross boundary call fails until someone exports the module deliberately. That is the intended direction of travel, since the cost of an unexported call is one line of configuration while the cost of a violated architecture is measured in months.
The same mechanism polices other applications
Boundary can also be used to manage calls to other applications, even if those applications do not define their own boundaries, and this is the feature that catches projects that are not strictly layered.
The rules it can enforce are given as an example: only MySystemWeb can use phoenix modules, only MySystem can use ecto modules except for Ecto.Changeset which can be used by MySystemWeb too, and only MySystemMix can use mix modules at runtime while everyone can use mix modules at compile time.
By default, boundary does not check calls to other apps, so the check has to be requested, either for each individual boundary or globally. The global form goes in mix.exs under a boundary key with a default check of apps, listing :phoenix and :ecto and a tuple of {:mix, :runtime}, which is how the runtime restriction is expressed for Mix while compile time calls stay unchecked.
The tuple form is a nice piece of design. Most dependency checkers are all or nothing, and the ability to say that a module may be used at compile time by everyone but at runtime by one boundary is the difference between a rule people work around and a rule people keep.
The per boundary side then grants the permissions, with MySystemWeb declaring deps: [Phoenix, Ecto.Changeset], MySystem declaring deps: [Ecto, Ecto.Changeset], and MySystemMix declaring deps: [Mix].
Ecto.Changeset becomes its own boundary, and that surprises people
There is a detail in the README's example that catches out anyone who reads quickly, and it is worth spelling out because it is the kind of thing that costs an afternoon.
In the MySystem boundary the configuration specifies both Ecto and Ecto.Changeset, and the reason given is that Ecto.Changeset is listed as a dep in MySystemWeb, and so it is treated as a separate boundary.
The mechanism follows from the export model. When a boundary declares a dependency on a module that another boundary also depends on, that module is shared, and the tool resolves the shared module as its own boundary rather than letting two boundaries silently claim the same code. The result is that permitting Ecto.Changeset in one place does not permit it in another, and both places have to say so.
Whether that is the behaviour you want is a judgement call, and it is the one design decision in this library a team should argue about before adopting it. The strict reading is that a module used by two boundaries is a coupling between them, and the tool is telling you so. The looser reading is that the tool is being conservative about a shared utility. Either way, the rule surfaces the question, which is more than most dependency checkers do.
The other point is about scope. Restricting an application to one boundary, like restricting all Ecto usage to MySystem, requires that every call site be inside a boundary that declares the dependency, which in a project without boundaries is every module. That is why the default is off, and why turning it on in a project that has not been through the boundary exercise first produces a long list of warnings rather than a small one.
Editor integration is a consequence of being a mix compiler
Because boundary is implemented as a mix compiler, it integrates with editors that can work with mix compilers, and the README gives VS Code with Elixir LS as the example, with screenshots of the warnings appearing in the editor.
That is the right kind of integration for this kind of tool. A dependency rule that is reported in a terminal is a rule that developers learn to ignore, because the terminal is where you look when something has already gone wrong, while the editor is where the mistake is made.
The mechanism is the standard Mix one. The dependency is added to the project with runtime: false, so it is a build time tool rather than something shipped with the application, and the compiler is added to the project's compiler list ahead of the default ones, as compilers: [:boundary] ++ Mix.compilers().
Because it runs as a compiler rather than as a test, the checks apply to every build, including the one your editor triggers on save. The cost is that a violation is a warning during compilation rather than a failed test, and a team that wants it to be a hard failure has to decide how to treat warnings in their build pipeline.
A young library with a stated scope
The Status section is brief and honest about the limits of the evidence. The library has been used in smaller production projects for about a year, and while it has not been tested on larger projects or umbrella projects, no issues are expected in those cases, and the project asks you to open an issue if you do run into problems.
That is a reasonable position for a compile time dependency, and it also tells you what to expect. An umbrella project is the case where the boundary model gets interesting, because a single umbrella with many applications has a different module namespace structure, and the automatic membership rule based on name prefixes is exactly the assumption that changes there.
The roadmap shows the same pattern of steady, small increments. Validate calls to external deps is done, so the Phoenix, Ecto and Mix checking described above is available. Support for compile time versus runtime dependencies is done, which is the mechanism behind the Mix rule. Support for nested boundaries, defining internal boundaries within a boundary, is done. Support for Erlang modules is not done, and that is the open item on the list.
The licence is MIT, and the project is published on Hex with the documentation on hexdocs, with the reference split between the Boundary module documentation and the Mix.Tasks.Compile.Boundary compiler documentation. The repository is not archived and the last push was on 2026-09-17, and there are no GitHub releases, which for a Hex package is the normal arrangement since the versions are published to the package registry instead.
The repository layout is a conventional Elixir project with lib/, test/ and demos/, where the demos directory holds the complete working example the README points to, and the tooling files for Credo, Dialyzer and the formatter, along with a .tool-versions file, tell you that the project takes its own quality gates seriously.
Editorial conclusion
Boundary suits an Elixir team that has decided on a layered architecture and wants the layering to hold under pressure, particularly one that has been bitten by a context module reaching into a controller. It does not suit a small application with two modules, and it is the wrong tool if the rule you want to express is a runtime one rather than a compile time one. Verify first which modules each boundary actually claims, because membership is derived from the module name prefix rather than declared, and check whether the boundary you want already exists as a dependency of another one, since a module used in two boundaries is treated as a separate boundary in its own right.
Frequently asked questions
What does the Boundary library do in an Elixir project?
It helps manage and restrain cross module dependencies. You declare boundaries as named groups of modules with an export list and a list of allowed dependencies, and a mix compiler reports every cross module function call that the configuration does not permit.
How do I add Boundary to a project?
Add it to the dependencies in mix.exs as {:boundary, "~> 0.11", runtime: false}, declare your boundaries with use Boundary, deps: [...] and exports: [...], and add :boundary to the compilers list in the project configuration.
Which modules end up in a Boundary?
They are determined automatically from the boundary name. The MySystem boundary contains the MySystem module and any module whose name starts with MySystem., for example MySystem.User or MySystem.User.Schema, so membership follows the naming convention rather than a declared list.
Can Boundary restrict the use of other applications such as Phoenix and Ecto?
Yes, once calls to external apps are enabled in the configuration, per boundary or globally, so for example only the web boundary may use Phoenix and only the context boundary may use Ecto, with Ecto.Changeset allowed in both.
Is Boundary production ready?
The README says the library has been used in smaller production projects for about a year and has not been tested on larger projects or umbrella projects, while no issues are expected in those cases. Support for Erlang modules is still an open item on the roadmap.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/sasa1977-boundary)Was this useful?