This episode covers modular programming and Java project architecture: the Java Platform Module System, modularizing applications with module-info.java and dependency encapsulation, clean architecture, package-by-feature, and layered architecture, plus the important builder, factory, strategy, and observer design patterns.

The larger the application, the more important its architecture. Episode 18 covers modular programming and project architecture — from the Java Platform Module System to the design patterns that form the shared language of Java developers. You will learn to structure applications that are easy to maintain and grow.
Good architecture keeps small changes safe and isolated. This episode equips you with the tools to build that foundation: modules, layers, and battle-tested design patterns.
JPMS, introduced in Java 9, adds a module system to the platform. Modules expose specific packages and declare their dependencies explicitly — providing much stronger encapsulation than the classpath.
A module is declared in a module-info.java file at the source root:
module com.aplikasi.core {
requires java.sql;
requires com.fasterxml.jackson.databind;
exports com.aplikasi.core.model;
exports com.aplikasi.core.service;
}requires declares a dependency, and exports specifies which packages other modules can access. Other packages remain hidden.
Compile and run with the module flags:
javac -d out --module-source-path src $(find src -name "*.java")
java --module-path out -m com.aplikasi.main/com.aplikasi.main.Main--module-path replaces the classpath, and -m specifies the module and the main class.
Break large applications into functional modules: core, service, web, and persistence. Each module depends only on what it needs and exposes only what is necessary.
Dependency encapsulation prevents accidental access to internal packages. This makes architectural boundaries enforced by the compiler, not just by team discipline.
Clean architecture separates layers: domain (business core), use cases, and adapters (web, database). Its main rule: dependencies point inward — inner layers do not depend on outer layers.
domain <- use case <- adapters (web, persistence, external)Package-by-feature groups code by feature, for example order, payment, and produk. Layered architecture groups by technical function: controller, service, repository. Package-by-feature is better for large applications because changes are localized per feature.
com.aplikasi.order -> OrderController, OrderService, OrderRepository
com.aplikasi.payment -> PaymentController, PaymentServiceBuilder simplifies creating objects with many parameters, especially for immutable objects:
public class Pengguna {
private final String nama;
private final int umur;
private Pengguna(Builder b) {
this.nama = b.nama;
this.umur = b.umur;
}
public static class Builder {
private String nama;
private int umur;
public Builder nama(String nama) { this.nama = nama; return this; }
public Builder umur(int umur) { this.umur = umur; return this; }
public Pengguna build() {
return new Pengguna(this);
}
}
}Factory centralizes object creation and hides the instantiation logic:
public interface Pembayaran {
void proses(double jumlah);
}
public class PembayaranFactory {
public static Pembayaran buat(String tipe) {
return switch (tipe) {
case "kartu" -> new PembayaranKartu();
case "transfer" -> new PembayaranTransfer();
default -> throw new IllegalArgumentException("Tipe tidak dikenal");
};
}
}Strategy allows algorithms to be swapped at runtime:
public interface StrategiDiskon {
double hitung(double harga);
}
public class DiskonLebaran implements StrategiDiskon {
public double hitung(double harga) {
return harga * 0.8;
}
}Observer makes an object notify its dependents when a change occurs — widely used in event systems:
import java.util.concurrent.*;
public class DemoObserver {
public static void main(String[] args) {
SubmissionPublisher<Integer> publisher = new SubmissionPublisher<>();
publisher.subscribe(new Flow.Subscriber<Integer>() {
public void onSubscribe(Flow.Subscription s) { s.request(Long.MAX_VALUE); }
public void onNext(Integer item) { System.out.println("Nilai: " + item); }
public void onError(Throwable t) {}
public void onComplete() {}
});
publisher.submit(42);
publisher.close();
}
}Episode 18 covers modular programming and architecture: JPMS with module-info.java and dependency encapsulation, clean architecture, package-by-feature, layered architecture, and the builder, factory, strategy, and observer design patterns.
Key takeaways:
requires and exports define the contract between modules.In the next episode, episode 19, we will discuss modern tooling and build automation — Maven versus Gradle Kotlin DSL, dependency management, modern plugins, the build lifecycle, code formatting, linting, automatic quality gates, reproducible build scripts, and multi-module projects. Time to automate the build!