Beyond an ArchUnit-style test that checks package dependency direction within one compilation unit, some teams split layers into physically separate build modules (e.g., separate Gradle modules or JARs) with explicit `dependencies {}` declarations. What additional guarantee does a physical module boundary provide that a same-source-tree architecture test does not, and what does it cost?
answer
- compile-time impossibility vs post-hoc test
- separate build files' dependency declarations per module
- unresolved reference if not declared
- complementary to arch tests, not a replacement
- shared-type ownership friction is the cost
basics
~20 sA test only catches a wrong-direction import when someone runs the test suite. A separate build module makes it flat-out impossible to compile a wrong-direction import in the first place, because the compiler can't even see the other module's code unless it's declared as a dependency.
solid answer
~50 sAn architecture test (ArchUnit, dependency-cruiser) runs after the code compiles, walking the already-built class/import graph and failing the test suite if it finds a forbidden edge — so a violation is technically compilable and could slip through if the test is skipped, disabled, or simply not run. A physical module boundary (a separate Gradle/Maven module, a separate compiled artifact) makes the violation impossible to compile at all: if the domain module's build file doesn't list the web module as a dependency, code in the domain module cannot reference a class from the web module — there's nothing to import, full stop, independent of whether anyone runs a test. The cost is real: more build files to maintain, potential per-module build overhead, and organizational friction any time a genuinely cross-cutting type needs to move or be shared, which usually forces introducing yet another shared/api module.
go deeper
Understands the basic distinction that a test needs to run to catch something, while the compiler always runs.
Can describe declaring module dependencies in a build file and what happens (a compile error) when an undeclared import is attempted.
Can articulate when architecture tests suffice versus when physical separation is worth the investment, referencing concrete costs on both sides.
Makes or influences the actual call for an organization — which boundaries earn physical module status, sequencing that decision against how stable those boundaries have proven, and weighing it against independent-deployability goals.
## How an architecture test protects An architecture test like ArchUnit or dependency-cruiser works by **inspecting a dependency graph that already exists** — it compiles (or bundles) the code first, then walks the resulting class/import graph and asserts that certain edges aren't present. That means the underlying language and build tool never actually prevented the forbidden import from being written or compiled; the protection is entirely downstream, in a check that has to be explicitly run and explicitly wired to fail the build. If that test is skipped — a developer runs a fast local build that excludes the test task, a CI step is misconfigured, someone comments the assertion out 'temporarily' — the forbidden import compiles and ships exactly as if the rule didn't exist, because nothing about the language itself objected. ## What a module boundary changes A physical module boundary changes where the protection lives, moving it from 'a test that inspects the graph after the fact' to **'the compiler literally cannot resolve the reference in the first place.'** Concretely: 1. Split the codebase into separate Gradle (or Maven) modules — say `:domain`, `:application`, `:web`, `:infrastructure` — each with its own build file declaring an explicit dependency block, e.g. `implementation(project(":domain"))`. 2. If `:domain`'s build file does not declare a dependency on `:web`, then no class inside `:domain` can import anything from `:web` at all — there's no classpath entry for it, so the import statement itself fails to compile with an unresolved-reference error, the same category of error as typing a class name that doesn't exist. This is a guarantee no test can match, because it doesn't depend on anyone remembering to run anything: it's enforced by the same mechanism that makes any other missing dependency a compile error, every single time, for every developer, including one running a bare module-level compile task with no test task involved at all. ## Complementary, not redundant This stronger guarantee comes from a different point in the toolchain than an architecture test, and the two are complementary rather than redundant. An ArchUnit test can express **fine-grained rules within a single module** that a build-module split can't — 'no class in `com.app.domain` may depend on a class in `com.app.web`' is a rule about packages, which is meaningful even when both packages compile within one module and share one classpath; a physical module split, by contrast, can only enforce boundaries at the granularity you're willing to create separate build modules for, which is coarser and more expensive to set up. Many mature codebases use both: physical modules for the handful of boundaries that matter most (the domain must never see the web framework, full stop, enforced structurally) and architecture tests for finer-grained rules within a module or for boundaries not worth the ceremony of a full separate build module. ## What physical modules cost The cost of physical modules is genuine and shows up in several places. - **Build files multiply** — every module needs its own build file, its own dependency declarations, and its own version/plugin configuration to keep consistent across the project, which is itself maintenance surface. - **Build tooling overhead can increase**: depending on the build system and its incremental-compilation model, splitting one compilation unit into several can add per-module task overhead, classpath assembly, and artifact publishing steps that a single module doesn't pay, though modern build tools mitigate a lot of this with incremental and parallel builds. - **The most consequential cost, though, is organizational friction** at the moment a genuinely cross-cutting type needs to exist: if `:domain` and `:web` both need a small shared value type, someone now has to decide whether it belongs in `:domain` (with `:web` depending on `:domain` for it), or whether it needs its own `:shared` or `:api` module — a decision an architecture-test-only setup never forces you to make explicitly, because within one module you can just put the type wherever's convenient. ## The friction is the point That extra friction is, from another angle, exactly the point: it forces the team to make an explicit ownership decision about the shared concept rather than letting it drift wherever was easiest at the time, at the cost of slowing down that one decision.
- Could a team get most of this benefit without fully separate Gradle modules, using something lighter-weight?Partially — some module systems (like the Java Platform Module System) offer stronger encapsulation than plain packages without the full ceremony of separate build projects, and some teams get a good chunk of the discipline just from a well-maintained architecture-test suite; but nothing short of a genuinely separate compilation unit gives the 'literally won't compile' guarantee, since that requires the compiler to not have the other module's classes on its classpath at all.
- If physical modules give a stronger guarantee, why don't most teams start with them from day one?The upfront cost (deciding module boundaries and dependency declarations before the design has stabilized) is high relative to the benefit early on, and getting the boundaries wrong early is expensive to undo across many build files; many teams deliberately start with a single module plus architecture tests, which are cheap to change, and only invest in physical module separation once the boundaries have proven stable and the team wants the stronger guarantee or genuine independent deployability.
- Does splitting into physical modules by itself guarantee dependencies point inward, or just that undeclared dependencies can't compile?Just the latter, on its own — physical modules only prevent a dependency the team never declared; nothing about module separation inherently stops someone from also, mistakenly, declaring a dependency in the wrong direction and creating a cycle at the module-graph level (which most build tools do reject as a circular project dependency, but that's a separate build-tool-level check) — the module dependencies still have to be declared in the correct direction for the inward-pointing property to hold.
An architecture test is a security guard checking badges after people have already walked into the building; physically separate modules are locked doors that only open for badges the building was actually built to accept — one relies on someone doing the check, the other makes the wrong entry impossible by construction.
saying these in an interview costs you the question
- Thinks architecture tests and physical modules are interchangeable/redundant
- Doesn't recognize that a skipped or misconfigured test provides zero protection
- Proposes splitting every package into its own build module regardless of scale/cost
- Can't name a concrete cost of physical module separation beyond vague 'it's more complex'