Using Ceylon Modules to Avoid Dependency Hell
Learn how Ceylon’s module system lets you expose only the API you choose, hide internal code, and keep dependencies from colliding across teams.
28 May 2026, 05:16 UTC

The real pain: leaking internals and version clashes
When a Ceylon library is published as a plain JAR, every public class ends up on the classpath of its consumers. Teams often accidentally depend on internal utility classes, and when two libraries require different versions of the same internal type, the build breaks or behaves unpredictably at runtime.
Thesis: a module descriptor forces a clean contract
Ceylon’s module system requires each library to ship a module.ceylon file that lists the exact version, the packages it exports, and any modules it imports. Anything not exported stays hidden, even if it is shared in source. The compiler enforces this contract, so consumers can only see the API you deliberately expose.
Worked example: building a tiny utility module
- Create a directory layout:
mkdir -p util-demo/src/com/example/util/strings cd util-demo
- Write the module descriptor (
module.ceylon) in the project root:module com.example.util "1.0.0" { export shared strings; } - Add a shared API in
src/com/example/util/strings.ceylon:shared String greet(String name) => "Hello, ``name``!";
- Optionally add an internal helper that should stay hidden:
private String internalHelper() => "do not use";
- Compile the module (run from the project root):
ceylon compile --src src --out modules
Required permission: read/write access to the current directory. The command creates a module archive under
modules/com.example.util/1.0.0/. - Verify that only the exported package is present:
jar tf modules/com.example.util/1.0.0/module.car
Expected output lists
com/example/util/strings.classbut no trace of aninternalpackage. - Consume the module from another project:
mkdir -p consumer/src/com/example/consumer cd consumer echo 'module com.example.consumer "1.0.0" { import com.example.util "1.0.0"; }' > module.ceylon echo 'shared void run() { print(com.example.util.strings.greet("World")); }' > src/com/example/consumer/run.ceylon - Compile the consumer:
ceylon compile --src src --out modules --modules-path ../util-demo/modules
If you try to reference
com.example.util.internalHelperthe compiler will emit an error, confirming the hidden contract.
Trade‑offs and limitations
- Learning curve: developers must maintain the descriptor file and understand the export/import syntax.
- Descriptor format changed between Ceylon 1.2 and 1.3; older projects may need to update the syntax.
- Modules are resolved at compile time; hot‑swapping or runtime replacement of a module (useful for some plugin architectures) is not supported.
- The module archive (
.car) is not a plain JAR, so tools that expect a standard JAR layout need the Ceylon runtime or a repackaging step.
Actionable next steps
Start by adding a module.ceylon file to any library you publish. Export only the packages that constitute your public API, keep helpers private or in a non‑exported package, and run ceylon compile to verify that the generated archive contains exactly what you expect. When consuming the module, attempt to reference a hidden symbol; a compiler error confirms that the encapsulation works. This practice reduces accidental dependencies, makes version conflicts explicit, and gives teams a reproducible way to share libraries on the JVM.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.