docs/architecture/adr/0009-api-internal-split.md
Thunderbird for Android uses a modular architecture. Many feature and core areas are split into api (public contracts)
and impl (implementation) modules, e.g. :feature:account:settings:api and :feature:account:settings:impl.
Over time, the main friction was ambiguity in naming and ownership:
api in package names made it unclear whether a type was part of a public contract.impl, internal, or none) led to confusion.api (stable contracts) vs. what should stay internal to the feature.To improve clarity and discoverability, we rename impl modules to internal and formalize module, package, and
content rules for both API and internal code — for both feature and core modules.
:feature:*:api modules define the public contracts exposed to other features.:feature:*:impl modules are renamed to :feature:*:internal, marking them as private implementation details.:core:*:api modules define public contracts exposed to feature and other core modules.:core:*:impl modules are renamed to :core:*:internal, marking them as private implementation details.:feature:*:api or :core:*:api of other areas. Depending on
:feature:*:internal or :core:*:internal from a different area is prohibited.:app-common
and the app-specific modules :app-k9mail and :app-thunderbird.:feature:*:internal and :core:*:internal modules, use the internal visibility modifier for all code that is not strictly required to be public for dependency injection or application composition. This ensures that implementation details are not accessible directly even when a module has a dependency on the internal module.Put only stable, intentionally shared contracts in api:
:app-common, app modules).Keep everything else in internal:
Notes for core modules:
api exposes stable, shared infrastructure contracts (e.g., logging, networking abstractions, serialization, clock, crypto interfaces). Core internal contains their implementations and wiring.:feature:<area>[:<subarea>]:api:feature:<area>[:<subarea>]:internal (formerly impl):core:<area>[:<subarea>]:api:core:<area>[:<subarea>]:internal (formerly impl)internal module with a qualifier, e.g. rename
:feature:mail:message:export:impl-eml to :feature:mail:message:export:internal-eml.:feature:*:api, use net.thunderbird.feature.<area>[.<subarea>], e.g.:
net.thunderbird.feature.account.settings, net.thunderbird.feature.mail.message.reader.:feature:*:internal, mirror the API package structure but place all implementation under an
.internal segment, e.g.: net.thunderbird.feature.account.settings.internal,
net.thunderbird.feature.mail.message.reader.internal.data, net.thunderbird.feature.mail.message.reader.internal.domain.:core:*:api, use net.thunderbird.core.<area>[.<subarea>], e.g.: net.thunderbird.core.network.:core:*:internal, mirror the API package structure and place implementation under .internal, e.g.:
net.thunderbird.core.network.internal, net.thunderbird.core.crypto.internal..internal
segment.
:…:internal-<variant> → package …internal.<variant>.:feature:mail:message:export:internal-eml → package root
net.thunderbird.feature.mail.message.export.internal.eml (e.g., …internal.eml.data, …internal.eml.domain).:feature:mail:message:export:internal-pdf →
net.thunderbird.feature.mail.message.export.internal.pdf.:core:network:internal-okhttp → net.thunderbird.core.network.internal.okhttp.:core:crypto:internal-bouncycastle → net.thunderbird.core.crypto.internal.bouncycastle.internal.<dimension>.<value>.
net.thunderbird.core.storage.internal.database.sqlite,
net.thunderbird.feature.search.internal.engine.lucene..internal for the variant part to keep packages readable.[!NOTE]
- Avoid adding
.apito package names for new code — the module already is the API.- Prefer small focused API packages with narrow sets of contracts; keep type stability in mind when promoting code to API.
- API packages should remain variant-agnostic in almost all cases; concrete variants live under
.internal.<variant>.
:feature:*:api only.:core:*:api only.:core:*:api only.:feature:* modules.:feature:*:internal and :core:*:internal dependencies are only allowed from:
api module (when strictly necessary), and:app-common, :app-k9mail, :app-thunderbird.:*:internal outside of these exceptions.impl to internal:
settings.gradle.kts to update:
:feature:account:avatar:impl → :feature:account:avatar:internal:feature:account:settings:impl → :feature:account:settings:internal:feature:mail:message:export:impl-eml → :feature:mail:message:export:internal-eml:core:<area>:impl → :core:<area>:internal (and for subareas accordingly)namespace in build.gradle.kts and Kotlin/Java package declarations to include .internal.:feature:*:internal and :core:*:internal (enforced
in the root build).:app-common or app modules.internal. Promote types to api only once they’re needed and stable.api vs. internal improves consistency.