DocsGuides

Multi-module applications

You do not scan a multi-module application one module at a time. You point Tibyaan at the whole application once, and it produces one manifest that covers every module. If you scanned a single module and saw only that module's capabilities, this is why, and this is the fix.

How scan scope works

tibyan scan <path> walks the entire directory tree under the path you give it and reads every .java file it finds. It skips only build and tooling directories (target, build, out, bin, .git, .idea, .mvn). It does not stop at a module boundary. Two consequences follow.

  1. One scanned root produces one manifest. The module field in the manifest is simply the name of the directory you scanned. It is a label for the source root, nothing more.
  2. Discovery is resolved against exactly the files you scan. Types in modules you did not scan are treated as external library types, the same as java.util.List. A read whose entity, repository, or return type lives outside the scanned root is invisible.

This is why scanning module by module and scanning the whole application can give different results. It is not a bug in either run; it is the direct meaning of "resolved from your own source". Scan at the scope you intend to serve.

Preview the whole application

Point the CLI at the reactor root, exactly as you would a single module. This reads source and writes nothing:

shell
java -jar tibyan-cli-0.3.0-jar-with-dependencies.jar scan /path/to/your-reactor-root --report survey.md

Where a single-module scan and a reactor scan disagree, the survey's refusal lines name the cause. The most common cross-module cause is an ambiguous simple type name: if two modules declare a type with the same simple name and a service references it by that bare name, Tibyaan refuses rather than guess which one is meant. A single module in isolation has no such ambiguity, so a read can be discovered there and refused at the wider scope, or the reverse.

Point the plugin at the reactor root

The build plugin runs once per module it is bound to, and by default scans only that module. For a multi-module application you want one manifest, covering the domain modules your app serves, on the classpath of the module that actually runs.

  1. Put the runtime dependency and the build plugin in the module that produces your runnable boot jar, the one with spring-boot-maven-plugin. That module's classpath is where the runtime looks for the manifest at startup.
  2. Point the plugin's sourceRoot at the reactor root, so it scans across module boundaries into one manifest:
pom.xml (the runnable module)
<plugin>
  <groupId>io.github.tibyaan-org</groupId>
  <artifactId>tibyan-maven-plugin</artifactId>
  <version>0.3.0</version>
  <configuration>
    <sourceRoot>${maven.multiModuleProjectDirectory}</sourceRoot>
  </configuration>
  <executions>
    <execution>
      <goals><goal>generate</goal></goals>
    </execution>
  </executions>
</plugin>

Confirm it resolved to the root you expect by checking the module field of the generated target/classes/tibyan/capabilities.json: it will be your reactor root's directory name. If it does not resolve correctly (it depends on where you invoke Maven and on your reactor layout), set an explicit path instead:

shell
mvn package -Dtibyan.sourceRoot=/absolute/path/to/your-reactor-root

Preview first, always

Run the CLI scan at the same root you will configure, read the survey, and adopt only if it convinces you. The plugin runs the identical scan, so what the preview shows at a given root is what the plugin will generate at that root.

Scope and cost

Scanning the whole reactor reads every module's source at build time. That is read-only and it is source parsing, not compilation, but it does mean the manifest reflects every module in scope. If you deliberately want to expose only one bounded context, scan only that context's source root instead. Match the scan scope to what you intend to serve.

Two modules, one entity name

Tibyaan keys entities by fully qualified class name, so two modules that legitimately reuse a simple entity name, for example billing.Invoice and sales.Invoice, are distinct and both survive, with disambiguated display names.

The one hard failure is a genuine split-package clash: the same fully qualified name declared by two source files under the scanned root. That fails loudly with a located error rather than silently merging them. It can only arise when a scan spans modules, and the message names the clashing key. See also the FAQ.

The authoritative text is docs/MULTI-MODULE.md (opens in a new tab) in the repository. If this page and that document ever disagree, the repository wins.