Modulith / bounded context isolation enforcement.
Define named slices (bounded contexts) and enforce that internals of one slice are not accessed by other slices. Only the public API module (the root context module) may be called cross-slice.
Slice structure
Given define_slices(orders: "MyApp.Orders", ...):
- Public API:
MyApp.Orders(the exact root module) - Internals:
MyApp.Orders.*and deeper (sub-modules)
Example
define_slices(
orders: "MyApp.Orders",
inventory: "MyApp.Inventory",
accounts: "MyApp.Accounts"
)
|> allow_dependency(:orders, :accounts)
|> enforce_isolation()
Summary
Functions
Asserts that every module under namespace_pattern belongs to a declared slice.
Permits from_slice to call the public API (root module) of to_slice.
Defines named slices (bounded contexts).
Defines slices by capturing one namespace segment from existing modules.
Enforces that slice internals are not accessed by other slices.
Sets the OTP application to introspect (default: :all).
Asserts that there are no circular dependencies between slices.
Asserts that slices have absolutely no cross-slice dependencies.
Types
@type slice_name() :: atom()
@type t() :: %ArchTest.Modulith{ allowed_deps: [{slice_name(), slice_name()}], app: atom() | nil, slices: [{slice_name(), String.t()}] }
Functions
Asserts that every module under namespace_pattern belongs to a declared slice.
Any module that does not match any slice's namespace is a violation. This prevents new modules from silently escaping slice coverage.
Options
:except— list of glob patterns to exclude from the check:graph— pre-built dependency graph (useful for testing, avoids xref)
Example
define_slices(auth: "Vireale.Auth", feeds: "Vireale.Feeds")
|> all_modules_covered_by("Vireale.**",
except: ["Vireale.Application", "Vireale.Repo"])
@spec allow_dependency(t(), slice_name(), slice_name()) :: t()
Permits from_slice to call the public API (root module) of to_slice.
Without this, cross-slice calls to internal sub-modules are always violations, and calls to other slices' public root modules are also violations by default.
With allow_dependency(:orders, :accounts), MyApp.Orders.* may call
MyApp.Accounts (but not MyApp.Accounts.Repo, etc.).
Defines named slices (bounded contexts).
Accepts a keyword list of slice_name: "RootNamespace" pairs.
Defines slices by capturing one namespace segment from existing modules.
The pattern must include one (*) segment. For example,
"MyApp.(*)" captures direct children like MyApp.Orders and creates
slices such as orders: "MyApp.Orders". A wildcard can appear before the
capture, such as "MyApp.*.(*)".
Options:
:app— OTP application to inspect:graph— pre-built graph, useful in tests:except— module glob patterns to ignore
Enforces that slice internals are not accessed by other slices.
Rules:
- Module A (in slice X) calling Module B (in slice Y's internals) is a violation. Internals are never callable from another slice.
- Calls to another slice's public root module require
allow_dependency(X, Y).
Sets the OTP application to introspect (default: :all).
Asserts that there are no circular dependencies between slices.
Each slice is treated as a single node; a cycle exists when slice A depends on slice B which (transitively) depends on slice A.
Asserts that slices have absolutely no cross-slice dependencies.
This is stricter than enforce_isolation/1 — not even the public root module
of another slice may be called. Use this for completely independent bounded contexts.
Example
define_slices(
orders: "MyApp.Orders",
inventory: "MyApp.Inventory",
accounts: "MyApp.Accounts"
)
|> should_not_depend_on_each_other()