codelessgenie blog

Why --add-modules is Necessary for Modules on the Java Module Path: JavaFX Compile Error Example Explained

Since Java 9 introduced the Java Platform Module System (JPMS), developers have gained powerful tools for encapsulation, dependency management, and modular design. However, with this new system comes new complexity—especially when working with modules that aren’t explicitly declared as dependencies. One common source of confusion is the --add-modules flag, which often becomes necessary when using libraries like JavaFX on the module path.

If you’ve ever encountered cryptic compile errors like package javafx.scene.control is not visible despite having JavaFX installed, you’re not alone. This blog demystifies why --add-modules is critical in such scenarios, using a step-by-step JavaFX example to clarify the underlying mechanics of the module system. By the end, you’ll understand when and how to use --add-modules to resolve dependency issues on the module path.

2026-01

Table of Contents#

  1. Understanding Java Modules and the Module Path
  2. The Problem: Unresolved Dependencies on the Module Path
  3. JavaFX Compile Error: A Real-World Example
  4. Demystifying --add-modules: What It Does and Why It’s Needed
  5. How to Use --add-modules Correctly
  6. Common Pitfalls and Best Practices
  7. Conclusion
  8. References

1. Understanding Java Modules and the Module Path#

Before diving into --add-modules, let’s recap the basics of the Java module system:

What is a Java Module?#

A module is a collection of packages and resources with a module-info.java descriptor that declares:

  • The module’s name (module com.example.myapp { ... }).
  • Dependencies on other modules (requires java.base; requires javafx.controls;).
  • Which packages to export for use by other modules (exports com.example.myapp.ui;).

The Module Path vs. Classpath#

  • Classpath: The traditional way to specify dependencies. Classes are loaded from JARs without module boundaries, leading to "JAR hell" (conflicts, missing dependencies).
  • Module Path: Used for modular JARs (with module-info.class). The module system enforces strict encapsulation: only exported packages are accessible, and dependencies are explicitly declared.

When you place a modular JAR on the module path, the module system treats it as an explicit module, subject to JPMS rules.

2. The Problem: Unresolved Dependencies on the Module Path#

The module system’s strength—strong encapsulation—is also its biggest source of confusion. By default, the module system resolves dependencies using a process called root module resolution:

  1. Start with a set of "root modules" (e.g., your application module).
  2. Resolve all transitive dependencies of these root modules (modules required by root modules, their required modules, etc.).

Modules not in this resolved graph are not available to your application, even if they’re on the module path. This is intentional: it prevents accidental dependency leaks and ensures only declared dependencies are used.

But what if your application needs a module that isn’t declared as a dependency (via requires in module-info.java)? Or if the module is optional and not a transitive dependency of any required module? In such cases, the module system will not resolve it, leading to errors like missing packages or classes.

3. JavaFX Compile Error: A Real-World Example#

JavaFX is a prime example of this issue. Since Java 11, JavaFX is no longer included in the JDK; it must be downloaded separately and added to the module path. Let’s walk through a scenario where a missing --add-modules flag causes a compile error.

Step 1: Set Up a Simple JavaFX App#

Create a basic JavaFX application with the following structure:

my-javafx-app/
├── src/
│   └── main/
│       └── java/
│           └── com/
│               └── example/
│                   └── App.java
└── javafx-sdk-21/  (JavaFX SDK, placed on the module path)

App.java:

package com.example;
 
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.stage.Stage;
 
public class App extends Application {
    @Override
    public void start(Stage stage) {
        Button button = new Button("Hello, JavaFX!");
        stage.setScene(new Scene(button, 300, 200));
        stage.show();
    }
 
    public static void main(String[] args) {
        launch(args);
    }
}

Step 2: Compile Without --add-modules#

Attempt to compile the app, specifying the JavaFX SDK on the module path:

javac --module-path /path/to/javafx-sdk-21/lib \
      -d out \
      src/main/java/com/example/App.java

Step 3: The Error#

You’ll see an error like this:

src/main/java/com/example/App.java:3: error: package javafx.application is not visible
import javafx.application.Application;
       ^
  (package javafx.application is declared in module javafx.graphics, which is not in the module graph)
src/main/java/com/example/App.java:4: error: package javafx.scene is not visible
import javafx.scene.Scene;
       ^
  (package javafx.scene is declared in module javafx.graphics, which is not in the module graph)
... (similar errors for javafx.scene.control and javafx.stage)

Why This Happens#

JavaFX is split into modular JARs (e.g., javafx-base.jar, javafx-controls.jar, javafx-graphics.jar), each with its own module-info.java. The javafx.graphics module exports javafx.application and javafx.scene, but your application hasn’t declared a dependency on javafx.graphics (via requires javafx.graphics; in module-info.java).

Since your app’s module (if it had one) doesn’t require JavaFX modules, and JavaFX modules aren’t transitive dependencies of any required modules (like java.base), the module system doesn’t resolve them. Thus, their packages are "not visible" to your code.

4. Demystifying --add-modules: What It Does and Why It’s Needed#

The --add-modules flag solves this problem by adding modules to the root set during resolution. In other words, it forces the module system to include specified modules (and their transitive dependencies) in the resolved graph, even if they aren’t declared as dependencies.

How Root Module Resolution Works Without --add-modules#

By default, the root modules are:

  • Your application module (if module-info.java exists).
  • The java.base module (implicitly required by all modules).

If your app has no module-info.java (i.e., it’s an unnamed module), the root modules are java.base plus any modules added via --add-modules.

Why JavaFX Needs --add-modules#

JavaFX modules (e.g., javafx.controls, javafx.graphics) are not dependencies of java.base or any other default root module. Thus:

  • If your app is an unnamed module (no module-info.java), the module system won’t resolve JavaFX modules unless explicitly added with --add-modules.
  • If your app is a named module but doesn’t require javafx.controls, the module system also won’t resolve JavaFX modules unless added with --add-modules.

5. How to Use --add-modules Correctly#

Command Line (javac/java)#

To compile and run the JavaFX app from Step 3, add --add-modules javafx.controls (since javafx.controls transitively requires javafx.graphics and javafx.base):

Compile:

javac --module-path /path/to/javafx-sdk-21/lib \
      --add-modules javafx.controls \
      -d out \
      src/main/java/com/example/App.java

Run:

java --module-path /path/to/javafx-sdk-21/lib \
     --add-modules javafx.controls \
     -cp out \
     com.example.App

The app will now compile and run successfully!

In Build Tools#

Maven#

Use the maven-compiler-plugin and maven-surefire-plugin to add --add-modules:

<build>
  <plugins>
    <!-- Compiler plugin -->
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <version>3.11.0</version>
      <configuration>
        <compilerArgs>
          <arg>--module-path</arg>
          <arg>${javafx.sdk.path}/lib</arg>
          <arg>--add-modules</arg>
          <arg>javafx.controls,javafx.fxml</arg> <!-- Add required JavaFX modules -->
        </compilerArgs>
      </configuration>
    </plugin>
 
    <!-- Runtime plugin (for 'mvn exec:java') -->
    <plugin>
      <groupId>org.codehaus.mojo</groupId>
      <artifactId>exec-maven-plugin</artifactId>
      <version>3.1.0</version>
      <configuration>
        <executable>java</executable>
        <arguments>
          <argument>--module-path</argument>
          <argument>${javafx.sdk.path}/lib</argument>
          <argument>--add-modules</argument>
          <argument>javafx.controls,javafx.fxml</argument>
          <argument>-cp</argument>
          <argument>${project.build.outputDirectory}</argument>
          <argument>com.example.App</argument>
        </arguments>
      </configuration>
    </plugin>
  </plugins>
</build>

Gradle#

In build.gradle, configure the compileJava and run tasks:

plugins {
    id 'application'
}
 
repositories {
    mavenCentral()
}
 
dependencies {
    // No need to declare JavaFX dependencies if using the SDK; just set the module path
}
 
application {
    mainClass = 'com.example.App'
}
 
compileJava {
    options.compilerArgs += [
        '--module-path', file('/path/to/javafx-sdk-21/lib').absolutePath,
        '--add-modules', 'javafx.controls,javafx.fxml'
    ]
}
 
run {
    jvmArgs += [
        '--module-path', file('/path/to/javafx-sdk-21/lib').absolutePath,
        '--add-modules', 'javafx.controls,javafx.fxml'
    ]
}

6. Common Pitfalls and Best Practices#

Pitfalls to Avoid#

  • Overusing --add-modules: Adding unnecessary modules bloats your application and defeats the module system’s purpose. Only add modules your app explicitly needs.
  • Ignoring module-info.java: If your app is a named module, prefer requires in module-info.java over --add-modules for required dependencies. Use --add-modules only for optional modules.
  • Incorrect Module Names: JavaFX modules have specific names (e.g., javafx.controls, not javafx). Check the module name in the JAR’s module-info.class if unsure.

Best Practices#

  • Use named modules when possible: Declare dependencies with requires in module-info.java for clarity and maintainability.
  • Add transitive modules sparingly: For example, javafx.controls requires javafx.graphics, so adding javafx.controls alone is sufficient.
  • Leverage build tools: Let Maven/Gradle manage --add-modules instead of hardcoding command-line flags.

7. Conclusion#

The --add-modules flag is a critical tool for resolving dependencies on the Java module path, especially when working with libraries like JavaFX that aren’t part of the default root modules. By forcing the module system to include specific modules in the resolved graph, it ensures your application can access the classes it needs—even if those modules aren’t declared as explicit dependencies.

Remember: Use --add-modules to add optional or unrequired modules to the root set, but prefer requires in module-info.java for core dependencies. With this knowledge, you’ll avoid common module path errors and build more robust Java applications.

8. References#