Designing Public APIs with Java Modules: Best Practices and Pitfalls

Illustration for Designing Public APIs with Java Modules: Best Practices and Pitfalls
By Last updated:

Designing a public API with Java Modules requires careful planning: deciding what to expose, what to hide, and how to evolve the API without breaking clients. In real-world enterprise systems, API stability is critical—microservices, frameworks, and libraries rely on contracts that must remain clear and predictable.

Think of modules like departments in an organization. The HR department shares job postings (APIs) with the public, but not internal payroll records (implementation details). Public APIs must be carefully curated to balance usability with security.


Core Principles of Public API Design in Modules

1. Use exports Wisely

Expose only the packages intended for clients:

module com.example.library {
    exports com.example.library.api;
    // implementation not exported
}

2. Keep Internal Code Hidden

By default, packages not listed in exports remain inaccessible. This enforces encapsulation.

// Not exported → clients cannot use
package com.example.library.internal;

3. Use requires for Dependencies

Declare dependencies explicitly:

module com.example.library {
    requires java.sql;
    exports com.example.library.api;
}

4. Consider requires transitive

If clients need a dependency transitively:

module com.example.library {
    requires transitive com.example.utils;
    exports com.example.library.api;
}

Best Practices for Public API Design

Best Practices

  • Clearly separate API packages and implementation packages
  • Use exports only for stable, documented APIs
  • Use requires transitive to reduce boilerplate for clients
  • Provide versioning policies for modular libraries
  • Add service interfaces using uses and provides for extensibility

Pitfalls

  • Exporting all packages (destroys encapsulation)
  • Exposing unstable or internal APIs prematurely
  • Using open unnecessarily (security risk)
  • Ignoring dependency version conflicts

Example: Library API Design

module-info.java

module com.example.mathlib {
    exports com.example.mathlib.api;
    requires transitive com.example.utils;
    uses com.example.mathlib.spi.MathExtension;
    provides com.example.mathlib.spi.MathExtension
        with com.example.mathlib.impl.AdvancedMathExtension;
}

This design:

  • Exports only api
  • Requires utils transitively
  • Uses service providers for extensibility

What's New in Java Versions?

  • Java 5–8 → N/A (Modules introduced in Java 9)
  • Java 9 → Introduction of JPMS, exports, requires, provides
  • Java 11 → Better tooling for modular libraries
  • Java 17 → Stability improvements in JPMS usage for large projects
  • Java 21 → No significant updates across Java versions for this feature

Real-World Analogy

Designing public APIs in modules is like publishing an official company handbook. Employees (modules) follow it, customers (clients) depend on it, and internal drafts remain private. Once published, the handbook must remain reliable, with updates carefully managed.


Summary & Key Takeaways

  • Public API design in JPMS requires deliberate exports
  • Internal code should remain hidden for encapsulation
  • Use requires transitive to simplify dependencies for clients
  • Service interfaces (uses/provides) enable extensibility
  • Avoid overexposing internals to keep APIs stable and secure

FAQ: Designing Public APIs with Modules

1. What is the difference between the classpath and module path?
Classpath loads everything blindly, module path enforces module boundaries.

2. Why do I get “package not visible” errors?
Because the package is not exported in module-info.java.

3. What does requires transitive do?
It exposes dependencies to downstream modules automatically.

4. How do open and opens differ?
open exposes the whole module for reflection, opens targets specific packages.

5. What are automatic modules?
Non-modular JARs treated as modules. Useful for migration, not for stable APIs.

6. How does JPMS improve API security compared to classpath?
It enforces strict encapsulation, preventing unwanted access to internals.

7. When should I use jlink vs jmod?
jmod packages modules, jlink builds runtime images with selected modules.

8. Can I evolve a public API without breaking clients?
Yes, by versioning carefully and avoiding removal of exported APIs.

9. How do I handle third-party libraries that aren’t modularized?
Use automatic modules temporarily or keep them on the classpath.

10. Do frameworks like Spring support modular API design?
Yes, though reflection-heavy frameworks may require opens.