Skip to Content
Course content

121: Multi-Module Projects

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this a lot when I'm reviewing PRs from developers moving from small scripts to professional systems: the belief that packages are the same thing as modules. You'll see a project with twenty different packages like com.app.service and com.app.repository, and the developer thinks they've "modularized" their code. They feel organized because the folders look clean.

The "Packages are Modules" Delusion

Here is why that mindset fails you in production. Imagine you're building a Payment Processing System. You have a com.payment.core package for your business logic and a com.payment.web package for your REST controllers. In a single-module project, any class in web can import any class in core, and vice versa. There is nothing stopping a developer (or you, at 3 AM) from accidentally importing a database-specific entity directly into a web controller, bypassing the service layer entirely.

Packages provide namespacing, but they don't provide enforcement. Even if you use protected or package-private visibility, you're still shipping one giant "Fat JAR." If you ever wanted to reuse your core logic in a different project—say, a command-line migration tool—you'd be forced to drag along every single web dependency, JSON library, and servlet container used by the web package, even though the migration tool doesn't need a website.

Enforcing Boundaries with Build-Level Modules

A true multi-module project (using Maven or Gradle) moves the boundary from the Java language level to the build system level. Instead of one pom.xml or build.gradle, you have a root project and several sub-projects, each with its own build file.

Let's look at how I'd actually structure that Payment System:


payment-root/
├── pom.xml (Parent)
├── payment-core/
│   └── pom.xml (Dependencies: JUnit, Hibernate)
├── payment-api/
│   └── pom.xml (Dependencies: payment-core, Spring Web)
└── payment-client/
    └── pom.xml (Dependencies: payment-core, Apache HttpClient)

Now, the magic happens in the dependency declarations. The payment-api module explicitly lists payment-core as a dependency. However, payment-core does not list payment-api. It is physically impossible for a class in the core module to import a class from the API module because the compiler doesn't even know the API module exists when it's building the core. I love this because it turns architectural guidelines ("Please don't call the controller from the service!") into hard compiler errors.

Managing the Parent-Child Relationship

You might wonder if this creates a nightmare of duplicating versions for your dependencies. It doesn't, provided you use the Parent POM (in Maven) or the plugins block (in Gradle) correctly. The root project acts as the "Source of Truth." You define the version of a library—like Log4j or Jackson—once in the root, and the sub-modules simply inherit that version.

This setup gives you "selective shipping." When you deploy your API, you ship the API JAR and the Core JAR. If you're building a lightweight internal tool, you only ship the Client and Core. You've stopped treating your application as a single monolith and started treating it as a collection of collaborating libraries.




📋 Practical Task

Build a Decoupled Inventory System

You are tasked with refactoring a monolithic Inventory application into a multi-module project to prevent "leakage" between the database logic and the user interface.

Requirements:

  • Create a root project named inventory-manager.
  • Create two sub-modules: inventory-domain and inventory-cli.
  • In inventory-domain, create a class Product and a service StockService that manages a simple list of products. This module should have no dependencies on the CLI module.
  • In inventory-cli, create a Main class that instantiates StockService to print the current inventory.
  • Configure your build file (Maven pom.xml or Gradle build.gradle) so that inventory-cli depends on inventory-domain.

Verification: Try to create a class in inventory-domain that imports a class from inventory-cli. Your build should fail. If it fails, you have successfully enforced the module boundary.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.