Ceylon's shared modifier and import/use: keeping a module API intentionally small
Ceylon forces API opt-in with the shared modifier and separates compile-time import from implementation use in module.ceylon, letting you keep a module surface small and auditable.
10 Mar 2026, 00:53 UTC

A library ships, users start importing helper types you never meant to expose, and six months later you cannot remove them without breaking builds. Ceylon makes that accidental exposure harder by forcing an explicit opt-in for API and by separating compile-time dependencies from runtime implementation dependencies.
The useful takeaway is that the shared modifier and the import versus use distinction in a module descriptor let you design a module surface you can reason about, and the compiler enforces it across compilation units.
Accidental surface is the real problem
In many languages public is the default and visibility is retrofitted. Ceylon inverts that. Declarations are non-shared unless marked shared, and a module is a first-class unit with its own descriptor file. The descriptor, module.ceylon, declares name, version and dependencies. That means the dependency graph is part of the language, not just build metadata.
Null-safety also participates at the boundary. Ceylon has distinct nullable types by default. Because shared declarations are the only ones visible outside the module, nullability assumptions cannot leak through an internal type you forgot to hide.
Modules declare import and use separately
A module descriptor lives at the root of a source tree as module.ceylon. It lists dependencies with two keywords.
import is a compile-time dependency. If a shared declaration uses a type in its signature, that type must be imported. The type becomes part of the public API surface.
use is an implementation-only dependency. It is available inside the module but does not appear in the signatures of shared declarations. This supports cleaner API surfaces by hiding implementation libraries.
Example descriptor for a core logging module:
module com.example.logging.core {1.0.0} {
import ceylon.collection \"1.3.0\";
use java.logging \"1.0\";
}The import of ceylon.collection is visible to consumers because shared signatures may reference it. The use of java.logging is internal.
Shared is opt-in API
Inside the module, shared marks a declaration intended for public use. Non-shared declarations are invisible to other modules even if they are public in the Java sense. The compiler enforces this at module boundaries.
Example source in the same module:
shared class Logger(shared String name) {
shared void log(String message) {}
}
class LogFormatterInternal(String pattern) {
String format(String msg) => pattern + msg;
}Logger is shared and therefore part of the module API. LogFormatterInternal is non-shared and cannot be referenced from another module. A consumer module that imports com.example.logging.core can only see Logger.
A consumer module descriptor might look like:
module com.example.app {1.0.0} {
import com.example.logging.core \"1.0.0\";
}From the consumer you can write Logger() and call log, but any attempt to reference LogFormatterInternal is a compile error. That is the practical check for API control.
Trade-offs and limits
Shared versus non-shared is compile-time enforcement, not a security boundary. Reflection or low level access can still reach non-shared members. Module naming and versioning follow Ceylon conventions and differ from Maven or Java Platform Module System, so mixing conventions can cause resolution confusion.
Tooling and ecosystem activity for Ceylon has reduced over time. Long term compatibility and community support may be limited for new projects.
How to audit your surface
Inspect a source tree for module.ceylon files and read import versus use clauses. List shared declarations in each module and compare them to what consumers actually need. Build a second module that depends on the first and confirm the compiler rejects access to non-shared members. Compare the module descriptor metadata with compiled artifacts to see how declared dependencies are recorded.
If you need a small, stable API with implementation freedom, Ceylon's explicit sharing and import/use split is a deliberate engineering decision. It costs more upfront annotation, but it makes accidental coupling visible at compile time.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.