Java
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
183: Migrating a Legacy Application to Modules
I've seen this movie before. You inherit a massive, ten-year-old codebase—let's call it the InventoryManager system—and your boss tells you it's time to "modernize" it by moving to the Java Module System (JPMS). The app is a tangle of com.store.core, com.store.db, and com.store.api, all shoved into a single classpath where every class can see every other class. It's a wild west of dependencies.
The "Export Everything" Trap
When most developers first tackle this, they take the path of least resistance. They create a module-info.java for every project in the build, and because they don't want to spend three days fighting IllegalAccessException errors, they just export every single package in the project. It looks something like this:
module com.store.db {
exports com.store.db.internal;
exports com.store.db.connection;
exports com.store.db.model;
requires com.store.core;
}
I call this the "Hammer Approach." You've technically moved to modules, but you've gained nothing. You've just mirrored the existing classpath chaos in a more verbose format. The problem here is that you've leaked your internal implementation details. If com.store.db.internal contains a helper class that manages raw SQL strings, and you export it, some developer in the web module will inevitably start using that helper class directly. You've now codified your technical debt into the module system, making it nearly impossible to refactor the database layer without breaking the rest of the app.
Defining Strict Boundaries and Handling Reflection
The better way—the way that actually makes the migration worth the headache—is to treat module-info.java as a design document. Instead of asking "What needs to be visible so this compiles?", ask "What is the actual public API of this component?"
In our InventoryManager, the db module should only export the com.store.db.api package. Everything else—the connection pooling, the raw JDBC logic, the internal mapping—stays hidden. But here is where you'll hit a wall: reflection. If you're using Hibernate or Spring, those frameworks need to touch your private fields. If you just export the package, you're exposing it to the whole world. If you don't, the framework crashes.
This is where you use opens instead of exports. By using opens com.store.db.model to org.hibernate.orm.core, you're telling Java: "I don't want other modules calling these classes at compile time, but I'm allowing this specific framework to peek inside them via reflection at runtime." It's a surgical strike. You keep your architecture clean while satisfying the requirements of your dependencies.
The Upfront Cost of Correctness
Now, I'll be honest with you: the surgical approach takes significantly longer. You will spend an afternoon chasing down a single ClassNotFoundException because you forgot to require a transitive dependency. You'll find that some of your legacy code relies on "circular dependencies" (Module A needs B, and B needs A), which JPMS strictly forbids. You'll be forced to extract a third module, com.store.common, just to break the cycle.
It feels like busy work at first. But the trade-off is that you finally have a compiler-enforced architecture. When a new developer joins the team and tries to instantiate a database internal class from the UI layer, the code simply won't compile. You've moved the "don't do that" rule from a README file that nobody reads into the language itself. That's the real win.
📋 Practical Task
Refactoring the InventoryManager Module Boundaries
You have been handed a legacy project with two modules: com.store.core and com.store.db. Currently, com.store.db is exporting everything, including its internal connection logic, which is causing architectural leakage.
Your Task: Modify the module-info.java files to secure the application boundaries. Perform the following:
- In
com.store.db, remove theexportsstatement forcom.store.db.internal. - In
com.store.db, add anopensstatement forcom.store.db.model, granting access specifically to theorg.hibernate.orm.coremodule to allow for database mapping without exposing the model to the rest of the application. - Ensure
com.store.corestill has arequiresdependency oncom.store.db. - Verify that a class in
com.store.corecan no longer import classes fromcom.store.db.internal.
There are no comments for now.