docs/architecture/module-structure.md
The Thunderbird for Android project is following a modularization approach, where the codebase is divided into multiple distinct modules. These modules encapsulate specific functionality and can be developed, tested, and maintained independently. This modular architecture promotes reusability, scalability, and maintainability of the codebase.
Each module should be split into two main parts: API and internal. This separation provides clear boundaries between what a module exposes to other modules and how it implements its functionality internally.
[!NOTE] Prior to ADR-0009, the project used
:implsuffix for implementation modules. This naming has been changed to:internalto better reflect that these modules contain private implementation details. The codebase is being gradually migrated from the old:implnaming to:internal. You may encounter both naming conventions in the existing code, but all new modules should use the:internalsuffix.
When a feature is complex, it can be further split into sub modules, allowing for better organization and smaller modules for distinct functionalities within a feature domain.
This approach promotes:
The API module defines the public contract that other modules can depend on. It should be stable, well-documented, and change infrequently.
The API module contains:
The API module should be minimal and focused on defining the contract that other modules can depend on. It should not contain any implementation details.
API modules should follow the naming convention:
feature:<feature-name>:api for feature modulescore:<core-name>:api for core modulesfeature:account:api
โโโ src/main/kotlin/net/thunderbird/feature/account
โ โโโ AccountManager.kt (interface)
โ โโโ Account.kt (entity)
โ โโโ AccountNavigation.kt (interface)
โ โโโ AccountType.kt (entity)
โ โโโ AccountExtensions.kt (extension functions)
When designing APIs, follow these principles:
The internal module depends on the API module but must not be depended upon by other modules (except for
composition modules: :app-common, :app-k9mail, and :app-thunderbird).
The internal module contains:
Internal modules should follow the naming convention:
feature:<feature-name>:internal for standard implementation detailsfeature:<feature-name>:internal-<variant> for variant-specific implementation detailscore:<core-name>:internal for core module implementation detailsWhen multiple implementations are needed, such as for different providers or platforms, they can be placed in separate modules and named accordingly:
feature:account:internal-gmail - Gmail-specific implementationfeature:account:internal-yahoo - Yahoo-specific implementationfeature:account:internal-noop - No-operation implementation for testingfeature:account:internal-gmail
โโโ src/main/kotlin/net/thunderbird/feature/account/internal/gmail
โ โโโ GmailAccountManager.kt
A complex feature internal module should apply Clean Architecture principles, separating concerns into:
feature:account:internal
โโโ src/main/kotlin/net/thunderbird/feature/account/internal
โ โโโ data/
โ โ โโโ repository/
โ โ โโโ datasource/
โ โ โโโ mapper/
โ โโโ domain/
โ โ โโโ repository/
โ โ โโโ entity/
โ โ โโโ usecase/
โ โโโ ui/
โ โโโ AccountScreen.kt
โ โโโ AccountViewModel.kt
internal module, everything should be marked with the internal visibility modifier by default. Only code explicitly required for dependency injection (e.g., Koin modules) or composition (if absolutely necessary) should remain public. This prevents accidental usage of implementation details even in modules that depend on the internal module (like :app-common).Testing modules provide test implementations, utilities, and frameworks for testing other modules. They are essential for ensuring the quality and correctness of the codebase.
The testing module contains:
Testing modules should follow the naming convention:
feature:<feature-name>:testing for feature-specific test utilitiescore:<core-name>:testing for core test utilities<module-name>:test for module-specific testsfeature:account:testing
โโโ src/main/kotlin/net/thunderbird/feature/account/testing
โ โโโ AccountTestUtils.kt
โ โโโ AccountTestMatchers.kt
Fake modules provide alternative implementations of interfaces for testing, development, or demonstration purposes. They are simpler than the real implementations and are designed to be used in controlled environments.
The fake module contains:
[!IMPORTANT] Fake modules should be limited to the most generic data and implementations. Specific use cases or test setups should be part of the actual test, not the fake module.
Fake modules should follow the naming convention:
feature:<feature-name>:fake for feature-specific fake implementationscore:<core-name>:fake for core fake implementationsfeature:account:fake
โโโ src/main/kotlin/net/thunderbird/feature/account/fake
โ โโโ FakeAccountRepository.kt
โ โโโ FakeAccountDataSource.kt
โ โโโ InMemoryAccountStore.kt
โ โโโ FakeAccountManager.kt
โ โโโ data/
โ โโโ FakeAccountData.kt
โ โโโ FakeAccountProfileData.kt
Common modules provide shared functionality that is used by multiple modules within a feature. They contain implementation details, utilities, and components that need to be shared between related modules but are not part of the public API.
The common module contains:
Common modules should follow the naming convention:
feature:<feature-name>:common for feature-specific common codecore:<core-name>:common for core common codefeature:account:common
โโโ src/main/kotlin/net/thunderbird/feature/account/common
โ โโโ AccountCommonModule.kt
โ โโโ data/
โ โ โโโ InMemoryAccountStateRepository.kt
โ โโโ domain/
โ โ โโโ AccountDomainContract.kt
โ โ โโโ input/
โ โ โ โโโ NumberInputField.kt
โ โ โโโ entity/
โ โ โโโ AccountState.kt
โ โ โโโ AccountDisplayOptions.kt
โ โ โโโ AuthorizationState.kt
โ โโโ ui/
โ โโโ WizardNavigationBar.kt
โ โโโ WizardNavigationBarState.kt
internal modifier for classes and functions that should not be part of the public APIThe module dependency diagram below illustrates how different modules interact with each other in the project, showing the dependencies and integration points between modules.
graph TB
subgraph APP[App Modules]
direction TB
APP_TB["`**:app-thunderbird**
Thunderbird for Android`"]
APP_K9["`**:app-k9mail**
K-9 Mail`"]
end
subgraph COMMON[App Common Module]
direction TB
COMMON_APP["`**:app-common**
Integration Code`"]
end
subgraph FEATURE[Feature Modules]
direction TB
FEATURE1["`**:feature:account:api**`"]
FEATURE2["`**:feature:account:internal**`"]
FEATURE3["`**:feature:settings:api**`"]
FEATURE_K9["`**:feature:k9Only:internal**`"]
FEATURE_TB["`**:feature:tbOnly:internal**`"]
end
subgraph CORE[Core Modules]
direction TB
CORE1["`**:core:ui:api**`"]
CORE2["`**:core:common:api**`"]
end
subgraph LIBRARY[Library]
direction TB
LIB1[Library 1]
LIB2[Library 2]
end
APP_K9 --> |depends on| COMMON_APP
APP_TB --> |depends on| COMMON_APP
COMMON_APP --> |integrates| FEATURE1
COMMON_APP --> |injects| FEATURE2
FEATURE2 --> FEATURE1
COMMON_APP --> |integrates| FEATURE3
APP_K9 --> |integrates| FEATURE_K9
APP_TB --> |integrates| FEATURE_TB
FEATURE1 --> |uses| CORE1
FEATURE3 --> |uses| CORE2
FEATURE_TB --> |uses| CORE1
FEATURE_K9 --> |uses| LIB2
CORE2 --> |uses| LIB1
classDef app fill:#d9e9ff,stroke:#000000,color:#000000
classDef app_module fill:#4d94ff,stroke:#000000,color:#000000
classDef common fill:#e6e6e6,stroke:#000000,color:#000000
classDef common_module fill:#999999,stroke:#000000,color:#000000
classDef feature fill:#d9ffd9,stroke:#000000,color:#000000
classDef feature_module fill:#33cc33,stroke:#000000,color:#000000
classDef core fill:#e6cce6,stroke:#000000,color:#000000
classDef core_module fill:#cc99cc,stroke:#000000,color:#000000
classDef library fill:#fff0d0,stroke:#000000,color:#000000
classDef library_module fill:#ffaa33,stroke:#000000,color:#000000
classDef legacy fill:#ffe6e6,stroke:#000000,color:#000000
classDef legacy_module fill:#ff9999,stroke:#000000,color:#000000
linkStyle default stroke:#999,stroke-width:2px
class APP app
class APP_K9,APP_TB app_module
class COMMON common
class COMMON_APP common_module
class FEATURE feature
class FEATURE1,FEATURE2,FEATURE3 feature_module
class FEATURE_K9 featureK9
class FEATURE_TB featureTB
class CORE core
class CORE1,CORE2 core_module
class LIBRARY library
class LIB1,LIB2 library_module
These rules must be strictly followed:
:feature:*:api or :core:*:api of other areas.:feature:*:internal or :core:*:internal from a different area is prohibited.:app-common, :app-k9mail, and :app-thunderbird.Determining the right granularity for modules is crucial for maintainability and scalability. This section provides guidelines on when to create new modules and how to structure them.
Create a new module when:
Split an existing module when:
Keep functionality in the same module when: