CodeGym /Courses /JAVA 25 SELF /Splitting a Project into Modules: Best Practices

Splitting a Project into Modules: Best Practices

JAVA 25 SELF
Level 60, Lesson 3
Available

1. When and why to split a project into modules

Why you shouldn’t put everything in a single module

If you’re writing a small lab exercise or "Hello, World!", the module system may seem excessive. But as the project grows—dozens and hundreds of classes, many packages, third-party libraries—chaos becomes inevitable. It’s like a library without shelves: while there are few books it’s tolerable, but later it’s hard to find anything. Modules are your shelves: they help bring order and hide the internals (implementation), leaving only the public façade (API) on display.

Why split into modules

  • Separation of concerns: each module is responsible for its own area (for example, database, business logic, UI).
  • Code reuse: a module can be used in another project.
  • Improved testability: modules can be tested independently.
  • Security and encapsulation: only the API is visible externally; the implementation is hidden.
  • Easier maintenance: fewer “magic” couplings, a clear dependency map.
  • Faster build and deploy: only changed modules are rebuilt.

When to split into modules

  • The project becomes too big for one developer (or the IDE starts to lag).
  • Clear parts emerge: core, ui, utils, api, impl.
  • You plan to reuse code in other projects.
  • There are external dependencies needed only by part of the project.
  • You need to hide implementation details (algorithms, internal classes).

2. Common modularity patterns

Below are popular splitting patterns suitable for learning and production projects.

“Onion” architecture (Onion Architecture)

The outer layer depends on the inner one, but not the other way around.

[ app (UI) ]
     ↓
[ core (logic) ]
     ↓
[ utils (utilities) ]
  • app — the outer module: GUI, web application, entry point (main).
  • core — business logic, models, services.
  • utils — helper classes.

Rule: the inner layer must not depend on the outer one. This way, core can be reused across different interfaces (console, web, desktop).

Separate modules for API and implementation

For libraries, it’s convenient to separate interfaces from their implementations:

[ mylib.api ]  ← exports interfaces only
[ mylib.impl ] ← contains implementations, not exported

Test modules

Tests are often moved to a separate module so they don’t end up in the production artifact.

[ app ]
[ core ]
[ core.tests ]

Layout for a learning project

myeditor/
  ├─ app/         ← entry point, application startup
  ├─ core/        ← business logic (working with files, text)
  └─ utils/       ← utilities (logging, parsing)

3. Dependencies between modules

In Java modules, dependencies are explicitly declared in module-info.java with the requires keyword. This improves readability and lets the compiler/JVM control API visibility via exports.

Dependency example

core/module-info.java

module myeditor.core {
    exports myeditor.core.api; // only the api package is visible externally
    requires myeditor.utils;   // we use the utilities
}

app/module-info.java

module myeditor.app {
    requires myeditor.core;    // we use core
    requires myeditor.utils;   // we can use utilities directly
}

Rules and best practices

  • Avoid cyclic dependencies. If A requires B and B requires A, that’s a design defect. Usually resolved by extracting a shared common/api module.
  • Minimise dependencies. Don’t add a module unless it’s truly needed.
  • Export the packages you use. Classes must be in packages declared via exports, otherwise you’ll get a compilation error.
  • Utilities should be as independent as possible. utils must not depend on business logic.

4. Hands-on: example of splitting a learning project into 3 modules

Folder structure

myeditor/
  ├─ app/
  │    ├─ src/
  │    │    └─ myeditor/app/Main.java
  │    └─ module-info.java
  ├─ core/
  │    ├─ src/
  │    │    ├─ myeditor/core/api/TextService.java
  │    │    └─ myeditor/core/impl/TextServiceImpl.java
  │    └─ module-info.java
  └─ utils/
       ├─ src/
       │    └─ myeditor/utils/Logger.java
       └─ module-info.java

module-info.java examples

core/module-info.java

module myeditor.core {
    exports myeditor.core.api;
    requires myeditor.utils;
}

app/module-info.java

module myeditor.app {
    requires myeditor.core;
    requires myeditor.utils;
}

utils/module-info.java

module myeditor.utils {
    exports myeditor.utils;
}

Code example (TextService)

myeditor/core/api/TextService.java

package myeditor.core.api;

public interface TextService {
    String toUpperCase(String text);
}

myeditor/core/impl/TextServiceImpl.java

package myeditor.core.impl;

import myeditor.core.api.TextService;

public class TextServiceImpl implements TextService {
    @Override
    public String toUpperCase(String text) {
        return text.toUpperCase();
    }
}

myeditor/app/Main.java

package myeditor.app;

import myeditor.core.api.TextService;
import myeditor.core.impl.TextServiceImpl;

public class Main {
    public static void main(String[] args) {
        TextService service = new TextServiceImpl();
        System.out.println(service.toUpperCase("hello, modules!"));
    }
}

How this looks in IntelliJ IDEA

  • Each directory is a separate Module in the project structure.
  • Each module has its own module-info.java at the root of src.
  • When you run main from app, the IDE will pick an appropriate module-path.
  • An attempt to use a class from a non-exported package will result in a compilation error.

5. Impact on the build: Maven/Gradle and modules

Maven

A multi-module project consists of a “parent” project and several “child” modules.

myeditor/
  ├─ pom.xml        ← parent
  ├─ app/
  │    └─ pom.xml
  ├─ core/
  │    └─ pom.xml
  └─ utils/
       └─ pom.xml

Example parent pom.xml

<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>myeditor</groupId>
  <artifactId>myeditor-parent</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>pom</packaging>

  <modules>
    <module>app</module>
    <module>core</module>
    <module>utils</module>
  </modules>
</project>

Highlights:

  • Maven takes module-info.java into account during compilation.
  • For running, use --module-path instead of --classpath.
  • If you forget exports or requires, you’ll get a compilation error.

Gradle

Multi-module setup is configured via settings.gradle and per-module build.gradle files.

settings.gradle

rootProject.name = 'myeditor'
include 'app', 'core', 'utils'

build.gradle for a module

plugins {
    id 'java'
}

java {
    modularity.inferModulePath = true
}

IntelliJ IDEA

  • IDEA can generate module-info.java when creating a Java module.
  • With Maven/Gradle, the module structure is imported automatically.
  • When running main from app, the IDE will configure the module-path.
  • Import/export dialogs inform you about package and module visibility.

Common mistakes when splitting into modules

Error #1: Cyclic dependencies between modules. If two modules declare requires for each other, the compiler will report an error. Usually this is a sign of a drifting architecture. The solution is to extract a shared api module or reconsider the boundaries.

Error #2: Using classes from non-exported packages. A class can be public, but if its package isn’t listed in exports in module-info.java, another module won’t see it. Result: compilation error.

Error #3: Forgot to add requires for a used module. An import from another module without a corresponding entry in module-info.java won’t compile. Always declare dependencies explicitly.

Error #4: Duplicate module names. Module names must be unique within the build (especially with Maven/Gradle). Duplicates break the build.

Error #5: Wrong directory structure. The module-info.java file must be located at the root of the module’s src. Otherwise the compiler won’t find the module.

Error #6: Incorrect module-path at runtime. When launching manually, use --module-path instead of --classpath, or you’ll get “module not found”.

1
Task
JAVA 25 SELF, level 60, lesson 3
Locked
Opening a communication channel for the event log 📢
Opening a communication channel for the event log 📢
1
Task
JAVA 25 SELF, level 60, lesson 3
Locked
Digital Address Book Architect 🗺️
Digital Address Book Architect 🗺️
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION