Splitting a Ceylon Project into Multiple Modules
Decide when to split Ceylon code into multiple modules, compare build and version trade-offs, and compile a two-module example with module.ceylon.
17 Jun 2026, 02:12 UTC

Decision and constraints
You are maintaining a Ceylon application and the source tree has grown past a few dozen files. You must decide whether to keep all code in one module declared in module.ceylon or split it into multiple modules with explicit imports. A Ceylon module is a named, versioned unit of code whose dependencies are declared explicitly rather than discovered by classpath scanning. The wrong choice makes refactoring slow or produces circular import errors that the Ceylon 1.3.x compiler reports only at build time.
Constraints in Ceylon 1.3.x:
- Every module needs a
module.ceylondescriptor with a name, version, and import list. - Module names should follow reverse-domain notation (for example,
com.example.core) and match the directory layout. - Only packages listed with
exportare visible to other modules. - Code in the default package cannot be imported by another module.
Useful takeaway. Split code into separate modules when subsystems release independently or when you want to enforce dependency direction with compiler errors. Keep code in one module when everything ships together. Always pin dependency versions in module.ceylon so the toolchain resolves the same artifacts on every machine.
Supported options compared
| Option | Best for | Visibility control | Build impact | Version coupling |
|---|---|---|---|---|
| Single module | Small applications and prototypes | Package-level shared only | One compile unit | None |
| Multiple modules, built together | Large codebase, single release train | Module boundary via export | Separate compile units, same version | High—changing one version requires editing every importer |
| Multiple modules, published separately | Shared libraries consumed by many applications | Module boundary via export | Independent builds | Low—consumers pin exact versions |
Trade-offs
A single module is the simplest starting point. You avoid maintaining several descriptors, but you lose compiler-enforced boundaries between packages. Multiple modules give you stronger encapsulation: if com.example.app does not import com.example.core.internal, the compiler rejects the code even if both packages exist in the same repository.
The cost is version churn. Because Ceylon imports are versioned, bumping com.example.core from 1.0.0 to 1.0.1 means updating every module that imports it. In a large multi-module repository this can become tedious, which is why the separately published option only makes sense when modules genuinely have independent release cycles.
Concrete implementation: two modules
This example splits a tiny application into a library module and an application module. The source root contains two module directories.
com/example/core/
module.ceylon
com/example/core/Greeting.ceylon
com/example/app/
module.ceylon
com/example/app/Main.ceylon
The library module declares its name, version, SDK import, and exported package.
module com.example.core "1.0.0" {
import ceylon.language "1.3.3";
export com.example.core;
}
shared class Greeting {
shared String sayHello() => "Hello, Ceylon";
}
The application module imports the library by module name and version.
module com.example.app "1.0.0" {
import ceylon.language "1.3.3";
import com.example.core "1.0.0";
}
import com.example.core {
Greeting
}
shared void run() {
print(Greeting().sayHello());
}
Commands and checks
Run these commands from the project root as a normal user with read access to the source files and write access to the compiler output directory (default is typically modules/ or output/, depending on your Ceylon setup).
- Verify the toolchain version:
Expected: output identifying Ceylon 1.3.x. Module semantics changed between early milestones and 1.3.x, so do not assume 1.2 behavior applies.ceylon version - Compile the application module:
Expected: exit code 0 and generated module output under the configured output directory.ceylon compile com.example.app - Inspect the resolved module graph (if your toolchain provides it):
Expected: documentation listingceylon doc com.example.appcom.example.coreas an imported dependency.
Risks: if the directory path does not match the module name in module.ceylon, compilation fails with a module-not-found error. If com.example.core omits export com.example.core;, the application module cannot see Greeting. If the version constraint in the importer is too strict—for example, requiring "1.0.0" while the provider publishes "1.0.0+beta"—resolution fails even though the code is compatible.
Diagnostic decision table
| Build failure | Likely cause | Check |
|---|---|---|
module com.example.core not found | Directory name or descriptor name mismatch | Confirm module.ceylon declares com.example.core and the package lives under com/example/core/. |
package com.example.core is not exported | Missing export statement | Add export com.example.core; to the provider's module.ceylon. |
version constraint unsatisfied | Version string mismatch between importer and provider | Align the version in the provider descriptor with the constraint in the importer descriptor. |
Limitations and verification
The Ceylon toolchain is not actively maintained at the level of mainstream JVM languages. Before relying on module resolution in a CI pipeline, run ceylon version on the build machine and pin that toolchain version. Changing a module version requires updating every importing module's descriptor, so prefer semantic versioning only when you can automate the update across the repository.
Practical verification: after editing descriptors, run ceylon compile and confirm exit code 0. Then run ceylon doc against the consuming module to confirm the imported module appears in the documentation. If the build breaks, revert the descriptor edits with your version-control system; there is no database or system state to roll back beyond the changed files.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.