Java packages are a fine "module" system if you're designing a single large cohesive application that has no dependencies or trust boundaries. However, packages are inadequate by themselves to tackle the problems that arise when you assemble an application from many different components, each of which are independently produced and versioned, and have different trust boundaries.

Java has an incredible open source community and there are amazing high-quality packages available to solve any problem under the sun; and so it is common for Java apps to have large dependency graphs. For example, take Google Guava, a popular and powerful Java library. It's so useful that other libraries depend on it. Let's say that our application uses two libraries called Component-A and Component-B, which both depend on Guava. However, Guava is constantly changing, and so it may arise that Component-A depends on Guava version 17 while Component-B depends on Guava version 20.

This is called a version conflict, and Java packages aren't enough to solve the problem well, though module systems can. For example, a module system allows you, as the application owner, to override the Guava version used by Component-A, replacing Guava-17 with Guava-20. If Component-A is compatible with Guava-20, then that resolves the problem. Another solution module systems provide is to allow both Guava-17 and Guava-20 to coexist in the same application, used separately by Component-A and Component-B. If those components are using Guava 'privately', as part of their implementation only, then this might also be an adequate resolution.

The default Java package system does not have a notion that there may be multiple implementations of a class like `com.google.common.base.Preconditions`, and that you may wish to select one version of it or another, or that you may want to use different versions in different parts of the application. It's not normally desirable to tamper with dependency versions or load multiple versions simultaneously, but version conflicts happen and module systems give you tools to deal with them when they occur. Fortunately, the behavior of Java packages is determined by Java ClassLoaders, and module systems can implement their own ClassLoaders to provide dynamic behaviors like these that go beyond vanilla Java's capabilities.

Since Java supports reflection, with regular Java packages, any class can access any other class and class member in the app, regardless of whether it's package private. These reflection capabilities are super useful; they're what enable frameworks like Spring, Guice, EasyMock, and Jackson. However, unrestricted reflective access is also a liability (viz. Ruby Monkey Patching), and it's something that you'd prefer to outright disable or at least sandbox in most components you depend on. But since these components might legitimately require reflection in their implementation, like using Guice for dependency injection or EasyMock for testing, you may not be able to disable reflection entirely; instead, module systems can help you sandbox reflection so that it operates within the confines of the module, to make the system simpler and more predictable.

Some module systems like OSGi also extend into the runtime behavior of your application, and help you manage complex applications. Certain very complex Java applications act a bit like an operating system: there are multiple services within the app that communicate with each other, and the module system isolates them for encapsulation reasons, loads them in the appropriate order, connects them together through exported interfaces, and provides management actions like reloading a module while the rest of the app keeps running. These module systems are for Java apps what systemd is for Linux (including the fact that some people love them while others think they're overly complex and should be avoided).