Modern Java: Build with Modules, Not the Class Path

Peter Bubenik · Netflix Tech · · Source
Image for Leave the Class Path in the Rearview Mirror

After studying this material, students should be able to:

  1. Explain the limitations of the traditional Java classpath model
  2. Understand how the Java Module System enables modern tooling
  3. Apply module descriptor syntax to define dependencies and metadata
  4. Describe how module naming and discovery works in existing repositories
  5. Understand module security and integrity concepts

Step-by-Step Teaching

Step 1: The Problem — Why Leave the Classpath Behind?

The Traditional Classpath Model

Traditional Java Development:
┌─────────────────────────────────────┐
│  Source Files → Compile → JAR files │
│  Runtime: -classpath jar1:jar2:jar3 │
│  Everything dumped into one flat    │
│  namespace (ALL-UNNAMED)            │
└─────────────────────────────────────┘

Core Problems This Creates

ProblemConsequence
No explicit dependency boundariesAny code can access any other code
ALL-UNNAMED module accessHides technical debt sources
No version information in descriptorsDependency versions managed separately
Poor CLI toolingAI agents struggle to navigate Java projects

Key Concept: ALL-UNNAMED

# This kind of flag became dangerously common:
--add-opens java.base/java.lang=ALL-UNNAMED

# Problems:
# ✗ Hides WHICH module is actually requesting access
# ✗ Bypasses encapsulation for everyone
# ✗ Becomes a security liability as Java moves toward
#   "Integrity by Default"

Think of it like this: ALL-UNNAMED is like giving a master key to an entire apartment building instead of one specific tenant


Step 2: The Java Module System Foundation

What Is a Module Descriptor?

A module-info.java file that explicitly declares:

  • Module name
  • What it requires (dependencies)
  • What it exports (public API)
  • What it opens (reflection access)
// Traditional module-info.java
module com.example.application {
    requires com.example.framework;
    exports com.example.application.api;
}

The Gap the Module System Left

Module System Arrived → Build Tools Already Mature
                              ↓
         Module descriptor became "just another file"
         to keep synchronized with build files (pom.xml, etc.)
                              ↓
              Two sources of truth = maintenance burden

Step 3: The New Approach — Module Descriptor as Complete Project Description

Enhanced Module Descriptor Syntax

The ja tooling extends module descriptors using documentation tags:

/**
 * @mainClass com.example.application.Main    ← Entry point metadata
 */
module com.example.application {
    requires com.example.framework; // @1.2.3  ← Version pinned inline
}

Breaking Down the Syntax

/**
 * @mainClass com.example.application.Main
 *     ↑
 *     Documentation tag carries project metadata
 */
module com.example.application {
              ↑
              Reverse-DNS naming convention (like packages)

    requires com.example.framework; // @1.2.3
                    ↑                      ↑
                    Dependency         Version as comment
}

Why This Matters

Before:                          After:
┌─────────────────┐             ┌──────────────────────┐
│ module-info.java│             │ module-info.java      │
│ pom.xml         │    →        │ (single source of     │
│ build.gradle    │             │  truth for everything)│
└─────────────────┘             └──────────────────────┘

Step 4: Tooling Architecture — Composable Tools

The ja Tool Family

┌─────────────────────────────────────────────────────┐
│                    ja (orchestrator)                 │
│         Provides CLI ergonomics + coordination       │
└──────────┬──────────────────────────────────────────┘
           │ discovers and delegates to:
    ┌──────┴────────────────────────────────┐
    │                                       │
┌───▼────┐  ┌─────┐  ┌──────┐  ┌────────┐ │
│  jig   │  │ dep │  │ doc  │  │ other  │ │
│(proxy) │  │tool │  │ tool │  │ tools  │ │
└────────┘  └─────┘  └──────┘  └────────┘ │
    └──────────────────────────────────────┘
    Each tool: standalone, composable, implements Tool/ToolProvider

Key Design Principles

// Tools implement standard Java interfaces:
public class MyTool implements Tool {
    // Can be run IN-PROCESS (no subprocess overhead)
    // Discoverable by the platform
    // Installable alongside standard JDK tools
}

Composability Example

# You can use ja as full orchestrator:
ja require com.example.framework@1.2.3

# OR compose individual tools directly:
jig resolve com.example.framework@1.2.3
dep check module-info.java
doc generate --module com.example.app

Analogy: Think of Unix philosophy — small tools that do one thing well and can be piped together


Step 5: Module Discovery and Naming

The Current State of Maven Central

1,000 Most Popular Maven Artifacts:
┌─────────────────────────────────────────┐
│ ████████░░░░░░░░░░░░░░  232 (23.2%)     │
│ Explicit module definitions             │
│                                         │
│ ████████░░░░░░░░░░░░░░  248 (24.8%)     │
│ Automatic module names declared         │
│                                         │
│ ████████████████████░░  520 (52%)       │
│ No module name at all                   │
└─────────────────────────────────────────┘

The Naming Problem

Module System Rule:
  Module Name = Namespace

Problem:
  "jackson-databind" artifact → what's the module name?
  com.fasterxml.jackson.databind? jackson.databind? other?

The Solution: Canonical Maven Module Coordinates

Format: pkg:maven/{group}/{module-name}

Example: pkg:maven/com.netflix/com.netflix.tools.ja
                    ↑                    ↑
              Verified DNS          Full module name
              namespace

Discovery Strategy (Waterfall)

1. Explicit module definition in artifact?
   → Use it directly ✓

2. Author published relocation POM at canonical coordinate?
   → Follow redirect ✓

3. Neither available?
   → Walk namespace from DNS root using Maven conventions
   → Infer coordinates from module name ✓

4. Popular module with non-DNS name?
   → Check bundled alias list ✓

The jig Module Proxy

jig presents all modules using filename-based naming conventions
                    ↓
Even automatic modules without stable names become safe to use

Step 6: Security Model — Explicit Authorization

The Problem with Current Access Flags

# Current reality (problematic):
java --add-opens java.base/java.lang=ALL-UNNAMED \
     --enable-native-access=ALL-UNNAMED \
     -jar myapp.jar

# Problems:
# ✗ Blanket permission to everything unnamed
# ✗ No audit trail of who needs what
# ✗ Violates "Integrity by Default" direction

New Model: Declared + Authorized Access

Step 1: Library DECLARES what access it needs
                    ↓
Step 2: Application EXPLICITLY AUTHORIZES it
                    ↓
Step 3: Resolution FAILS if authorization missing

Concrete Example

// LIBRARY declares its requirements:
/**
 * @enableFinalFieldMutation com.example.framework
 *   ↑
 *   "I need to mutate final fields — here's who I am"
 */
module com.example.framework {
    // module body
}
// APPLICATION must explicitly authorize:
/**
 * @mainClass com.example.application.Main
 * @enableFinalFieldMutation com.example.framework
 *   ↑
 *   "I knowingly authorize THIS specific module"
 */
module com.example.application {
    requires com.example.framework; // @1.2.3
}
# CLI adds both dependency and authorization together:
ja require com.example.framework@1.2.3 \
   --enable-final-field-mutation com.example.framework

# Without authorization → resolution FAILS with clear error
# ✓ No silent permission grants
# ✓ Full audit trail in module descriptor
# ✓ Code review can see all authorizations

Access Types Supported

Annotation TagPurpose
@enableFinalFieldMutationAllow modifying final fields
@enableNativeAccessAllow native/foreign memory access
Qualified exports/opensFine-grained package-level access

Step 7: Module Integrity

Hash-Based Verification

Resolution Time:                    Subsequent Runs:
┌──────────────────┐               ┌──────────────────────┐
│ Resolve deps     │               │ Read hashes from      │
│ Compute hashes   │    →  →  →   │ module-info.hash      │
│ Store in         │               │ Verify each artifact  │
│ module-info.hash │               │ Reject if changed ✗   │
└──────────────────┘               └──────────────────────┘
# module-info.hash (conceptual):
com.example.framework@1.2.3 = sha256:a3f8b2...
com.example.other@2.0.0     = sha256:9c4d1e...

Why this matters: Protects against supply chain attacks where a published artifact is silently replaced

Annotation Processing Security

Traditional:                        New Approach:
┌─────────────────────┐            ┌──────────────────────────┐
│ Annotation processor│            │ Annotation processing =   │
│ runs silently during│    →       │ EXPLICIT code gen step    │
│ compilation         │            │ Generated sources visible │
│ Output hidden       │            │ in code review            │
└─────────────────────┘            │ Module assemblable WITHOUT│
                                   │ running generator code    │
                                   └──────────────────────────┘

Summary: The Complete Picture

┌─────────────────────────────────────────────────────────────┐
│              Modern Java Module Development                  │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  module-info.java = SINGLE SOURCE OF TRUTH                  │
│  ├── Module name (reverse DNS)                              │
│  ├── Dependencies + versions (inline comments)              │
│  ├── Metadata (doc tags: @mainClass, etc.)                  │
│  ├── Access authorizations (@enableNativeAccess, etc.)      │
│  └── Integrity hashes (module-info.hash companion)          │
│                                                             │
│  TOOLING                                                    │
│  ├── ja: CLI orchestrator                                   │
│  ├── jig: module proxy for discovery                        │
│  └── Composable standalone tools (Tool/ToolProvider)        │
│                                                             │
│  DISCOVERY                                                  │
│  ├── Canonical coordinates: pkg:maven/{group}/{module}      │
│  ├── DNS-verified namespaces                                │
│  └── Fallback inference strategies                          │
│                                                             │
│  SECURITY                                                   │
│  ├── Explicit access authorization (no ALL-UNNAMED)         │
│  ├── Hash-based artifact integrity                          │
│  └── Visible annotation processing                          │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Quick Knowledge Check

Q1: Why is ALL-UNNAMED considered harmful?

It grants blanket access without identifying which module needs it, hiding technical debt

Q2: What makes the new module descriptor a "complete" project description?

It combines module structure, dependency versions, entry point metadata, and access authorizations in one file

Q3: What happens if an application doesn't authorize a library's declared access requirement?

Dependency resolution fails with an explicit unsatisfied access requirement error

Q4: How does jig help with the 52% of artifacts that have no module name?

It presents resolved modules using filename-based naming conventions, making even automatic modules safe to use

More to study