Like a mille crΓͺpe β your architecture, one clean layer at a time.
mille is a static analysis CLI that enforces dependency rules for layered architectures β Clean Architecture, Onion Architecture, Hexagonal Architecture, and more.
One TOML config. Rust-powered. CI-ready. Supports multiple languages from a single config file.
Versioning policy
0.0.z: Under active development. No compatibility guarantees.0.y.z(y β 0): Compatibility maintained within the samey.x.y.z(x β 0): Stable release with full operational readiness.
Languages: Rust, Go, TypeScript, JavaScript, Python, Java, Kotlin, PHP, C, YAML
| Check | Description |
|---|---|
dependency_mode |
Layer dependency rules β control which layers can import from which |
external_mode |
External library rules β restrict third-party package usage per layer |
allow_call_patterns |
Method call rules β limit which methods may be called on any layer's types |
name_deny |
Naming convention rules β forbid infrastructure keywords in domain/usecase |
cargo install millenpm install -g @makinzm/mille
mille checkOr without installing globally:
npx @makinzm/mille checkRequires Node.js β₯ 18. Bundles mille.wasm β no native compilation needed.
go install github.com/makinzm/mille/packages/go/mille@latestEmbeds mille.wasm via wazero β fully self-contained binary.
# uv (recommended)
uv add --dev mille
uv run mille check
# pip
pip install mille
mille checkPre-built binaries are on GitHub Releases:
| Platform | Archive |
|---|---|
| Linux x86_64 | mille-<version>-x86_64-unknown-linux-gnu.tar.gz |
| Linux arm64 | mille-<version>-aarch64-unknown-linux-gnu.tar.gz |
| macOS x86_64 | mille-<version>-x86_64-apple-darwin.tar.gz |
| macOS arm64 | mille-<version>-aarch64-apple-darwin.tar.gz |
| Windows x86_64 | mille-<version>-x86_64-pc-windows-msvc.zip |
mille init
mille init ./path/to/project # specify target directorymille init analyzes actual import statements in your source files to infer layer structure and dependencies β no predetermined naming conventions needed. It prints the inferred dependency graph before writing the config:
Detected languages: rust
Scanning imports...
Using layer depth: 2
Inferred layer structure:
domain β (no internal dependencies)
usecase β domain
external: anyhow
infrastructure β domain
external: serde, tokio
Generated 'mille.toml'
| Flag | Default | Description |
|---|---|---|
--output <path> |
mille.toml |
Write config to a custom path |
--force |
false | Overwrite an existing file without prompting |
--depth <N> |
auto | Layer detection depth from project root |
--depth and auto-detection: mille init automatically finds the right layer depth by trying depths 1β6, skipping common source-layout roots (src, lib, app, etc.), and selecting the first depth that yields 2β8 candidate layers. For a project with src/domain/entity, src/domain/repository, src/usecase/ β depth 2 is chosen, rolling entity and repository up into domain. Use --depth N to override when auto-detection picks the wrong level.
The generated config includes allow (inferred internal dependencies) and external_allow (detected external packages) per layer. After generating, review the config and run mille check to see results.
Naming in monorepos: When multiple sub-projects contain a directory with the same name (e.g. crawler/src/domain and server/src/domain), mille init gives each its own layer with a distinguishing prefix (crawler_domain, server_domain). Merging is left to you.
Excluded paths: mille check automatically skips .venv, venv, node_modules, target, dist, build, and similar build/dependency directories, so generated paths patterns like apps/** are safe to use.
Python submodule imports: external_allow = ["matplotlib"] correctly allows both import matplotlib and import matplotlib.pyplot.
Python src/ layout (namespace packages): When your project uses a src/ layout and imports like from src.domain.entity import Foo, mille init detects that src is used as a top-level import prefix and automatically adds it to package_names. This means from src.domain... is classified as Internal and src does not appear in external_allow. Cross-layer imports like from src.domain.entity import Foo (written in src/infrastructure/) are correctly resolved to the src/domain layer and appear as an allow dependency in the generated mille.toml. Files at the project root of a sub-tree (e.g. src/main.py) are included in the src layer rather than being silently skipped.
Go projects: mille init reads go.mod and generates [resolve.go] module_name automatically β internal module imports are classified correctly during mille check. External packages appear in external_allow with their full import paths (e.g. "github.com/cilium/ebpf", "fmt", "net/http").
TypeScript/JavaScript subpath imports: external_allow = ["vitest"] correctly allows both import "vitest" and import "vitest/config". Scoped packages (@scope/name/sub) are matched by "@scope/name".
Java/Kotlin projects: mille init uses package declarations β not directory depth β to detect layers. This works correctly for Maven's src/main/java/com/example/myapp/domain/ as well as flat src/domain/ layouts. pom.xml (Maven) and build.gradle + settings.gradle (Gradle) are read automatically to generate [resolve.java] module_name. Layer paths use **/layer/** globs so mille check matches regardless of the source root depth.
Detected languages: java
Scanning imports...
Inferred layer structure:
domain β (no internal dependencies)
infrastructure β domain
external: java.util.List
usecase β domain
main β domain, infrastructure, usecase
Generated 'mille.toml'
Place mille.toml in your project root:
Rust:
[project]
name = "my-app"
root = "."
languages = ["rust"]
[[layers]]
name = "domain"
paths = ["src/domain/**"]
dependency_mode = "opt-in"
allow = []
external_mode = "opt-in"
external_allow = []
[[layers]]
name = "usecase"
paths = ["src/usecase/**"]
dependency_mode = "opt-in"
allow = ["domain"]
external_mode = "opt-in"
external_allow = []
[[layers]]
name = "infrastructure"
paths = ["src/infrastructure/**"]
dependency_mode = "opt-out"
deny = []
external_mode = "opt-out"
external_deny = []
[[layers]]
name = "main"
paths = ["src/main.rs"]
dependency_mode = "opt-in"
allow = ["domain", "infrastructure", "usecase"]
external_mode = "opt-in"
external_allow = ["clap"]
[[layers.allow_call_patterns]]
callee_layer = "infrastructure"
allow_methods = ["new", "build", "create", "init", "setup"]TypeScript / JavaScript:
[project]
name = "my-ts-app"
root = "."
languages = ["typescript"]
[resolve.typescript]
tsconfig = "./tsconfig.json"
[[layers]]
name = "domain"
paths = ["domain/**"]
dependency_mode = "opt-in"
allow = []
external_mode = "opt-out"
external_deny = []
[[layers]]
name = "usecase"
paths = ["usecase/**"]
dependency_mode = "opt-in"
allow = ["domain"]
external_mode = "opt-in"
external_allow = ["zod"]
[[layers]]
name = "infrastructure"
paths = ["infrastructure/**"]
dependency_mode = "opt-out"
deny = []
external_mode = "opt-out"
external_deny = []Use
languages = ["javascript"]for plain.js/.jsxprojects (no[resolve.typescript]needed).
Go:
[project]
name = "my-go-app"
root = "."
languages = ["go"]
[resolve.go]
module_name = "github.com/myorg/my-go-app"
[[layers]]
name = "domain"
paths = ["domain/**"]
dependency_mode = "opt-in"
allow = []
[[layers]]
name = "usecase"
paths = ["usecase/**"]
dependency_mode = "opt-in"
allow = ["domain"]
[[layers]]
name = "infrastructure"
paths = ["infrastructure/**"]
dependency_mode = "opt-out"
deny = []
[[layers]]
name = "cmd"
paths = ["cmd/**"]
dependency_mode = "opt-in"
allow = ["domain", "usecase", "infrastructure"]Python:
[project]
name = "my-python-app"
root = "."
languages = ["python"]
[resolve.python]
src_root = "."
package_names = ["domain", "usecase", "infrastructure"]
[[layers]]
name = "domain"
paths = ["domain/**"]
dependency_mode = "opt-in"
allow = []
external_mode = "opt-out"
external_deny = []
[[layers]]
name = "usecase"
paths = ["usecase/**"]
dependency_mode = "opt-in"
allow = ["domain"]
external_mode = "opt-out"
external_deny = []
[[layers]]
name = "infrastructure"
paths = ["infrastructure/**"]
dependency_mode = "opt-out"
deny = []
external_mode = "opt-out"
external_deny = []Java / Kotlin:
[project]
name = "my-java-app"
root = "."
languages = ["java"] # or ["kotlin"] for Kotlin projects
[resolve.java]
module_name = "com.example.myapp"
[[layers]]
name = "domain"
paths = ["src/domain/**"]
dependency_mode = "opt-in"
allow = []
external_mode = "opt-out"
[[layers]]
name = "usecase"
paths = ["src/usecase/**"]
dependency_mode = "opt-in"
allow = ["domain"]
external_mode = "opt-out"
[[layers]]
name = "infrastructure"
paths = ["src/infrastructure/**"]
dependency_mode = "opt-in"
allow = ["domain"]
external_mode = "opt-in"
external_allow = ["java.util.List", "java.util.Map"]
module_nameis the base package of your project (e.g.com.example.myapp). Imports starting with this prefix are classified as Internal and matched against layer globs. All other imports (includingjava.util.*stdlib) are classified as External and subject toexternal_allow/external_denyrules.
Before enforcing rules, you can inspect the actual dependency graph:
mille analyze # human-readable terminal output (default)
mille analyze --format json # machine-readable JSON graph
mille analyze --format dot # Graphviz DOT (pipe to: dot -Tsvg -o graph.svg)
mille analyze --format svg # self-contained SVG image (open in a browser)
mille analyze ./path/to/project # specify target directoryExample SVG output (dark theme, green edges):
mille analyze --format svg > graph.svg && open graph.svgmille analyze always exits 0 β it only visualizes, never enforces rules.
mille report external # human-readable table (default)
mille report external --format json # machine-readable JSON
mille report external --output report.json --format json # write to file
mille report external ./path/to/project # specify target directoryShows which external packages each layer actually imports β useful for auditing external_allow lists or documenting your dependency footprint.
Example output:
External Dependencies by Layer
domain (none)
usecase (none)
infrastructure database/sql
cmd fmt, os
mille report external always exits 0 β it only reports, never enforces rules.
mille check
mille check ./path/to/project # specify target directoryOutput formats:
mille check # human-readable terminal output (default)
mille check --format github-actions # GitHub Actions annotations (::error file=...)
mille check --format json # machine-readable JSONFail threshold:
mille check # exit 1 on error-severity violations only (default)
mille check --fail-on warning # exit 1 on any violation (error or warning)
mille check --fail-on error # explicit default β same as no flagExit codes:
| Code | Meaning |
|---|---|
0 |
No violations (or only warnings without --fail-on warning) |
1 |
One or more violations at the configured fail threshold |
3 |
Configuration file error |
| Key | Description |
|---|---|
name |
Project name |
root |
Root directory for analysis |
languages |
Languages to check: "rust", "go", "typescript", "javascript", "python", "java", "kotlin", "php", "c", "yaml" |
| Key | Description |
|---|---|
name |
Layer name |
paths |
Glob patterns for files in this layer |
dependency_mode |
"opt-in" (deny all except allow) or "opt-out" (allow all except deny) |
allow |
Allowed layers (when dependency_mode = "opt-in") |
deny |
Forbidden layers (when dependency_mode = "opt-out") |
external_mode |
"opt-in" or "opt-out" for external library usage |
external_allow |
Allowed external packages (when external_mode = "opt-in") |
external_deny |
Forbidden external packages (when external_mode = "opt-out") |
name_deny |
Forbidden keywords for naming convention check (case-insensitive partial match) |
name_allow |
Substrings to strip before name_deny check (e.g. "category" prevents "go" match inside it) |
name_targets |
Targets to check: "file", "symbol", "variable", "comment", "string_literal", "identifier" (default: all) |
name_deny_ignore |
Glob patterns for files to exclude from naming checks (e.g. "**/test_*.rs") |
Forbid infrastructure-specific keywords from appearing in a layer's names.
[[layers]]
name = "usecase"
paths = ["src/usecase/**"]
dependency_mode = "opt-out"
deny = []
external_mode = "opt-out"
external_deny = []
# Usecase layer must not reference specific infrastructure technologies
name_deny = ["gcp", "aws", "azure", "mysql", "postgres"]
name_allow = ["category"] # "category" contains "go" but should not be flagged
name_targets = ["file", "symbol", "variable", "comment", "string_literal", "identifier"] # default: all targets
name_deny_ignore = ["**/test_*.rs", "tests/**"] # exclude test files from naming checksRules:
- Case-insensitive (
GCP=gcp=Gcp) - Partial match (
ManageGcpalso matchesgcp) name_allowstrips listed substrings before matching (e.g."category"prevents false positive on"go")name_deny_ignoreexcludes files matching glob patterns from naming checks entirelyname_targetsrestricts which entity types are checked:"file": file basename (e.g.aws_client.rs)"symbol": function, class, struct, enum, trait, interface, type alias names"variable": variable, const, let, static declaration names"comment": inline comment content"string_literal": string literal content"identifier": attribute/field access identifiers (e.g.gcpincfg.gcp.bucket)
- Supported languages: Rust, TypeScript, JavaScript, Python, Go, Java, Kotlin, PHP, C, YAML
- Severity is controlled by
severity.naming_violation(default:"error")
Restricts which methods may be called on a given layer's types. Can be defined on any layer.
| Key | Description |
|---|---|
callee_layer |
The layer whose methods are being restricted |
allow_methods |
List of method names that are permitted |
Exclude files from the architecture check entirely, or suppress violations for test/mock files.
| Key | Description |
|---|---|
paths |
Glob patterns β matching files are excluded from collection and not counted in layer stats |
test_patterns |
Glob patterns β matching files are still counted in layer stats but their imports are not violation-checked |
[ignore]
paths = ["**/mock/**", "**/generated/**", "**/testdata/**"]
test_patterns = ["**/*_test.go", "**/*.spec.ts", "**/*.test.ts"]When to use paths vs test_patterns:
paths: Files that should not be analyzed at all (generated code, vendor directories, mocks)test_patterns: Test files that intentionally import across layers (e.g., integration tests that import both domain and infrastructure)
Control the severity level of each violation type. Violations can be "error", "warning", or "info".
| Key | Default | Description |
|---|---|---|
dependency_violation |
"error" |
Layer dependency rule violated |
external_violation |
"error" |
External library rule violated |
call_pattern_violation |
"error" |
DI entrypoint method call rule violated |
unknown_import |
"warning" |
Import that could not be classified |
naming_violation |
"error" |
Naming convention rule violated (name_deny) |
[severity]
dependency_violation = "warning" # treat as warning for gradual adoption
external_violation = "error"
call_pattern_violation = "error"
unknown_import = "warning"
naming_violation = "warning" # treat as warning while rolling out naming rulesUse --fail-on warning to exit 1 even for warnings when integrating into CI gradually.
| Key | Description |
|---|---|
tsconfig |
Path to tsconfig.json. mille reads compilerOptions.paths and resolves path aliases (e.g. @/*) as internal imports. |
How TypeScript / JavaScript imports are classified:
| Import | Classification |
|---|---|
import X from "./module" |
Internal |
import X from "../module" |
Internal |
import X from "@/module" (path alias in tsconfig.json) |
Internal |
import X from "react" |
External |
import fs from "node:fs" |
External |
| Key | Description |
|---|---|
module_name |
Go module name (matches go.mod). mille init generates this automatically from go.mod. |
| Key | Description |
|---|---|
src_root |
Root directory of the Python source tree (relative to mille.toml) |
package_names |
Your package names β imports starting with these are classified as internal. e.g. ["domain", "usecase"] |
How Python imports are classified:
| Import | Classification |
|---|---|
from .sibling import X (relative) |
Internal |
import domain.entity (matches package_names) |
Internal |
import os, import sqlalchemy |
External |
| Key | Description |
|---|---|
module_name |
Base package of your project (e.g. com.example.myapp). Imports starting with this prefix are classified as Internal. Generated automatically by mille init. |
pom_xml |
Path to pom.xml (relative to mille.toml). groupId.artifactId is used as module_name when module_name is not set. |
build_gradle |
Path to build.gradle (relative to mille.toml). group + rootProject.name from settings.gradle is used as module_name when module_name is not set. |
How Java imports are classified:
| Import | Classification |
|---|---|
import com.example.myapp.domain.User (starts with module_name) |
Internal |
import static com.example.myapp.util.Helper.method |
Internal |
import java.util.List, import org.springframework.* |
External |
Both regular and static imports are supported. Wildcard imports (
import java.util.*) are not yet extracted by the parser.
| Key | Description |
|---|---|
namespace |
Base namespace of your project (e.g. App). Imports starting with this prefix are classified as Internal. |
composer_json |
Path to composer.json (relative to mille.toml). The first PSR-4 key in autoload.psr-4 is used as the base namespace when namespace is not set. |
How PHP imports are classified:
| Import | Classification |
|---|---|
use App\Models\User (starts with namespace) |
Internal |
use App\Services\{Auth, Logger} (group use, expanded) |
Internal |
use function App\Helpers\format_date |
Internal |
use DateTime, use PDO, use Exception |
Stdlib |
use Illuminate\Http\Request |
External |
Supported use forms: simple, aliased (
as), grouped ({}),use function,use const. PHP stdlib classes (DateTime, PDO, Exception, etc.) are automatically classified as Stdlib without any configuration.
Example mille.toml for a Laravel project:
[project]
name = "my-laravel-app"
root = "."
languages = ["php"]
[[layers]]
name = "domain"
paths = ["app/Domain/**"]
[[layers]]
name = "application"
paths = ["app/Application/**"]
dependency_mode = "opt-in"
allow = ["domain"]
[[layers]]
name = "infrastructure"
paths = ["app/Infrastructure/**"]
dependency_mode = "opt-in"
allow = ["domain", "application"]
[resolve.php]
composer_json = "composer.json" # auto-detects "App\\" from autoload.psr-4
#include "..."is classified as Internal (project header).#include <...>is classified as Stdlib (standard/POSIX headers) or External (third-party libraries).
Example mille.toml for a C project:
[project]
name = "my-c-app"
root = "."
languages = ["c"]
[[layers]]
name = "domain"
paths = ["src/domain/**"]
[[layers]]
name = "usecase"
paths = ["src/usecase/**"]
dependency_mode = "opt-in"
allow = ["domain"]
[[layers]]
name = "infrastructure"
paths = ["src/infrastructure/**"]
dependency_mode = "opt-in"
allow = ["domain"]YAML is a naming-only language β it has no imports, so only
name_denychecks are supported.dependency_modeandexternal_modeshould be set to"opt-out".
Example mille.toml for YAML config files:
[project]
name = "my-k8s-project"
root = "."
languages = ["yaml"]
[[layers]]
name = "config"
paths = ["config/**"]
dependency_mode = "opt-out"
external_mode = "opt-out"
name_deny = ["aws", "gcp"]
[[layers]]
name = "manifests"
paths = ["manifests/**"]
dependency_mode = "opt-out"
external_mode = "opt-out"mille uses tree-sitter for AST-based import extraction β no regex heuristics.
mille.toml
β
βΌ
Layer definitions
β
Source files (*.rs, *.go, *.py, *.ts, *.js, *.java, *.yaml, ...)
β tree-sitter parse
βΌ
RawImport list
β Resolver (stdlib / internal / external)
βΌ
ResolvedImport list
β ViolationDetector
βΌ
Violations β terminal output
mille checks its own source code on every CI run:
mille check # uses ./mille.tomlSee mille.toml for the architecture rules applied to mille itself.
- spec.md β Full specification (in Japanese)
- docs/TODO.md β Development roadmap