Clean Architecture¶
The Cryptographer application follows Clean Architecture principles as defined by Robert C. Martin (Uncle Bob). This ensures the business logic remains independent of frameworks, UI, and external systems.
Core Concept¶
A solid architecture postpones decisions until the last responsible moment.
The best architecture often emerges late in development, once the codebase takes shape. Clean Architecture helps achieve this by separating concerns into distinct layers.
Dependency Rule¶
The fundamental rule of Clean Architecture is the Dependency Inversion Principle:
Dependencies point inward toward the domain.
This means: - Inner layers (Domain) have no dependencies on outer layers - Outer layers depend on inner layers through interfaces - Business logic is isolated from frameworks and UI
Layer Structure¶
┌─────────────────────────────────────┐
│ Presentation Layer (UI) │
│ - Compose Screens │
│ - ViewModels │
│ - UI Components │
└──────────────┬──────────────────────┘
│ depends on
┌──────────────▼──────────────────────┐
│ Application Layer (Use Cases) │
│ - Commands │
│ - Queries │
│ - Orchestration │
└──────────────┬──────────────────────┘
│ depends on
┌──────────────▼──────────────────────┐
│ Domain Layer (Business) │
│ - Entities │
│ - Value Objects │
│ - Domain Services │
│ - Business Rules │
└─────────────────────────────────────┘
▲
│ implemented by
┌──────────────┴──────────────────────┐
│ Infrastructure Layer (Adapters) │
│ - Storage Adapters │
│ - ID Generators │
│ - External Services │
└─────────────────────────────────────┘
Layer Details¶
Domain Layer (Inner)¶
Pure business logic with no Android dependencies.
Rules: - ✅ Uses only Kotlin standard library - ✅ No Android imports - ✅ No framework dependencies - ✅ Communicates via interfaces
Contains:
- Entities (Text, EncryptionKey, EncryptedText)
- Value Objects (EncryptionAlgorithm, TextEncoding)
- Domain Services (AesEncryptionService, ChaCha20EncryptionService)
- Domain Events
Example:
// Domain entity - no Android dependencies
data class EncryptionKey(
val id: KeyId,
val algorithm: EncryptionAlgorithm,
val keyBytes: ByteArray
) {
fun isValid(): Boolean {
return keyBytes.size == algorithm.keySizeInBytes
}
}
Application Layer¶
Orchestrates domain logic through commands and queries.
Rules: - ✅ Depends only on Domain layer - ✅ Uses CQRS pattern - ✅ Handles application-specific errors - ✅ Coordinates domain services
Contains: - Command handlers - Query handlers - View models (DTOs) - Application errors
Example:
// Command handler - orchestrates domain logic
class AesGenerateAndSaveKeyCommandHandler @Inject constructor(
private val encryptionService: AesEncryptionService,
private val keyStorage: KeyCommandGateway
) {
suspend fun handle(command: AesGenerateAndSaveKeyCommand): Result<KeyView> {
// Use domain service
val key = encryptionService.generateKey(command.algorithm)
// Save via infrastructure adapter
keyStorage.save(key)
return Result.success(key.toView())
}
}
Infrastructure Layer¶
Adapters that connect domain to external systems.
Rules: - ✅ Implements domain interfaces - ✅ Handles Android-specific code - ✅ Manages external dependencies - ✅ Converts between domain and external models
Contains: - Storage adapters (SharedPreferences, Room, etc.) - ID generators - External service clients
Example:
// Infrastructure adapter - implements domain interface
class KeyCommandGatewayAdapter @Inject constructor(
private val context: Context
) : KeyCommandGateway {
override suspend fun save(key: EncryptionKey) {
// Android-specific storage implementation
val prefs = context.getSharedPreferences("keys", Context.MODE_PRIVATE)
prefs.edit().putString(key.id.value, key.toJson()).apply()
}
}
Presentation Layer¶
UI components and state management.
Rules: - ✅ Depends on Application layer - ✅ Uses Jetpack Compose - ✅ Manages UI state - ✅ Handles user interactions
Contains: - Compose screens - ViewModels - UI components - Navigation
Example:
// ViewModel - coordinates UI and application layer
@HiltViewModel
class KeyGenerationViewModel @Inject constructor(
private val generateKeyCommand: AesGenerateAndSaveKeyCommandHandler
) : ViewModel() {
private val _uiState = MutableStateFlow(KeyGenerationUiState())
val uiState: StateFlow<KeyGenerationUiState> = _uiState.asStateFlow()
fun generateKey(algorithm: EncryptionAlgorithm) {
viewModelScope.launch {
generateKeyCommand.handle(
AesGenerateAndSaveKeyCommand(algorithm)
).fold(
onSuccess = { _uiState.value = it },
onFailure = { /* handle error */ }
)
}
}
}
Benefits¶
1. Testability¶
Domain logic can be tested without Android dependencies:
@Test
fun `encryption key validation works correctly`() {
val key = EncryptionKey(
id = KeyId("test"),
algorithm = EncryptionAlgorithm.AES_256,
keyBytes = ByteArray(32)
)
assertTrue(key.isValid())
}
2. Independence¶
Business logic is independent of: - UI framework (can switch from Compose to XML) - Storage mechanism (can switch from SharedPreferences to Room) - External libraries
3. Maintainability¶
Clear separation makes code: - Easier to understand - Easier to modify - Less prone to bugs
4. Scalability¶
Easy to add new features: - New encryption algorithm? Add to Domain layer - New UI screen? Add to Presentation layer - New storage? Implement Infrastructure adapter
Data Flow Example¶
Encrypting text:
1. User enters text in UI (Presentation)
↓
2. ViewModel calls command handler (Application)
↓
3. Command handler uses encryption service (Domain)
↓
4. Service encrypts text (Domain)
↓
5. Command handler saves via storage adapter (Infrastructure)
↓
6. ViewModel updates UI state (Presentation)
↓
7. UI displays encrypted text (Presentation)
Learn More¶
- CQRS Pattern - Command/Query separation
- Dependency Injection - Hilt setup
- Domain Layer - Domain layer details
- Application Layer - Application layer details