Choosing Shared vs Default Visibility in Ceylon Module APIs
Learn when to expose Ceylon declarations with shared visibility and when to keep them package‑private, with a decision table, trade‑off discussion, and a minimal validation example.
28 Apr 2026, 18:04 UTC

Decision and constraints
When designing a Ceylon module you must decide which top‑level declarations belong to the public API and which remain internal. The decision affects downstream consumers, versioning guarantees, and the risk of breaking changes when you refactor. The two visibility options are:
shared– makes the declaration part of the module’s exported API; any depending module can import and use it.default(package‑private) – limits the declaration to the containing module; it cannot be accessed from outside, even if the module is depended on.
The constraints to consider are:
- API stability – only
sharedelements are covered by semantic versioning guarantees. - Encapsulation –
defaulthides implementation details that may change. - Consumer ergonomics – exposing too many
sharedsymbols can clutter the API and lock down internal types.
Options comparison
| Visibility | Accessibility | Typical use | API stability impact | Refactoring risk |
|---|---|---|---|---|
shared | Visible to any module that declares a dependence | Interfaces, data transfer objects, stable constants | Part of the public contract; changes require version bump | High – changing a shared symbol can break downstream code |
default | Visible only within the same module | Helper classes, internal functions, private data structures | Not part of the contract; free to evolve | Low – changes stay inside the module boundary |
Trade‑offs
Using shared for stable interfaces gives consumers a reliable surface to build against, enabling safe semantic versioning. However, over‑exposing internal types as shared locks those implementations; any future refactor that alters the type signature forces a major version bump or breaks users.
Keeping implementation details default preserves flexibility. The downside is that if a consumer mistakenly relies on a default symbol (e.g., via reflection), the code will compile but may fail at runtime when the internal symbol changes or is removed.
A common pragmatic pattern is:
sharedfor interfaces, abstract classes, and DTOs that constitute the module’s contract.defaultfor concrete helper classes, utility functions, and any code that is likely to evolve.
Implementation example
The following minimal example shows a module com.example.api that exports a shared interface and keeps a helper class default. A second module com.example.app depends on the API and attempts to use both symbols.
// File: source/com/example/api/module.ceylon
module com.example.api "1.0.0" {
shared import ceylon.collection "1.3.3";
shared interface Greeter {
shared String greet(String name);
}
// helper is package‑private
class GreeterImpl() satisfies Greeter {
shared actual String greet(String name) => "Hello, ``name``!";
}
}
// File: source/com/example/app/module.ceylon
module com.example.app "1.0.0" {
import com.example.api "1.0.0";
shared void run() {
value g = GreeterImpl(); // ERROR: GreeterImpl is not visible
value i: Greeter = object satisfies Greeter {
shared actual String greet(String name) => "Hi, ``name``!";
};
print(i.greet("Ceylon"));
}
}
Place the two modules in separate directories, each with its own source folder containing the module.ceylon file shown above.
Validation steps
- Compile the API module:
ceylon compile --out=api-modules com.example.api - Attempt to compile the consumer module:
ceylon compile --out=app-modules --repopath=api-modules com.example.app - Observe the compiler error:
GreeterImplcannot be found because it isdefaultand therefore not exported. - To verify the effect of changing visibility, edit
GreeterImplin the API module, replaceclass GreeterImpl()withshared class GreeterImpl(), recompile the API, and repeat step 2. The consumer should now compile successfully. - Optionally, inspect the generated module descriptor:
unzip -l api-modules/com.example.api/1.0.0/module.xmland confirm that onlyshareddeclarations appear in theexportssection.
Limitations and practical checks
This example demonstrates compile‑time visibility; it does not guarantee that a default symbol cannot be accessed via reflection at runtime. If you rely on reflection, you must treat any default element as part of the API contract and version it accordingly.
To check that your visibility choices match your intent, run the compiler with --verbose and look for lines like Exporting shared declaration …. Only declarations marked shared should appear in the export list.
Remember that changing a shared type (e.g., adding a method) requires a minor or major version bump depending on whether the change is backward compatible. Keep a changelog or use Ceylon’s built‑in versioning tools to avoid surprising downstream users.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.