How Clean Architecture Is Implemented in the Bitchat Android Codebase

Bitchat Android implements Clean Architecture through four concentric layers—Presentation, Domain, Data, and Platform—enforcing strict inward-facing dependencies that keep Android framework code isolated from business logic.

The permissionlesstech/bitchat-android repository demonstrates a mature Clean Architecture implementation that organizes code around feature-focused packages while maintaining loosely-coupled layer boundaries. This architectural approach separates concerns into distinct rings, making the core application logic highly testable and keeping platform-specific dependencies confined to the outermost edges of the system.

Layered Package Structure

The Clean Architecture implementation in Bitchat Android organizes source files into four concentric layers, each with specific responsibilities and dependency constraints.

Presentation Layer (UI)

The Presentation layer resides in com.bitchat.android.ui and contains Jetpack Compose screens plus ViewModels that expose state via StateFlow or LiveData. In app/src/main/java/com/bitchat/android/ui/MainViewModel.kt, the ViewModel serves as the primary state holder, consuming domain-level abstractions like NostrRelayManager while remaining completely agnostic to networking implementations or platform APIs.

Domain Layer (Business Logic)

The Domain layer contains pure Kotlin classes that encapsulate core application logic without Android framework dependencies. Located in packages such as com.bitchat.android.nostr and com.bitchat.android.identity, this layer includes NostrRelayManager.kt and SecureIdentityStateManager.kt. These files orchestrate business rules—such as cryptographic identity management and Nostr protocol handling—independently of how data is persisted or transmitted.

Data and Infrastructure Layer

The Data layer implements interfaces defined by the domain, handling interactions with external systems like Bluetooth Low Energy (BLE), Tor networking, and file I/O. Files in com.bitchat.android.service and com.bitchat.android.mesh—including MeshForegroundService.kt and NoiseEncryptionService.kt—provide concrete implementations that satisfy domain contracts while managing Android-specific concerns like background execution permissions and hardware access.

Platform Layer (External)

The outermost Platform layer contains low-level native code and JNI bridges in info.guardianproject.arti. The ArtiNative.kt file provides the bridge to the Arti Tor implementation, representing the infrastructure boundary that depends on Android NDK and native libraries. This layer is completely isolated from business logic; it is only invoked through abstractions managed by the data layer.

Dependency Rules and Boundaries

The dependency direction in Bitchat Android always flows inward: UI → Domain → Data → Platform. UI components never invoke platform APIs directly; instead, they reference use-case classes from the domain layer, which in turn call repository interfaces implemented in the data layer. This strict boundary ensures that changes to networking implementations, cryptographic libraries, or native code remain confined to their respective rings without cascading into business logic or presentation code.

Key Architectural Patterns

ViewModel-Driven UI

Each screen maintains an associated ViewModel that holds UI state and interacts with domain-level interactors. The MainViewModel depends on NostrRelayManager and MeshForegroundService abstractions, consuming StateFlow emissions while remaining unaware of how those events are produced or transmitted across the mesh network.

Repository Pattern

The domain layer defines abstraction interfaces for data operations, satisfied by concrete implementations in the data layer. Domain code depends on interfaces like NostrRelayRepository, while NostrRelayRepositoryImpl in the data layer handles actual OkHttp network requests. This inversion of dependencies ensures that business rules never directly import okhttp3 or Android networking classes.

Service Facades

Background services expose thin façades that preserve separation of concerns. MeshForegroundService provides a public API consumed via coroutine flows in the presentation layer, preventing direct coupling between UI components and low-level networking code that manages BLE scanning or Tor circuit establishment.

Clean Architecture Code Examples

The following examples demonstrate how Clean Architecture separates concerns across the codebase:

ViewModel Consuming Domain Use-Case

class MainViewModel(
    private val nostrRelayManager: NostrRelayManager,
    private val meshService: MeshForegroundService
) : ViewModel() {

    private val _state = MutableStateFlow<UiState>(UiState.Idle)
    val state: StateFlow<UiState> = _state.asStateFlow()

    init {
        viewModelScope.launch {
            nostrRelayManager.events.collect { event ->
                _state.value = UiState.NewEvent(event)
            }
        }
    }
}

This MainViewModel lives in the presentation layer and depends only on domain-level abstractions.

Domain-Level Use-Case Orchestrating Mesh Networking

class PeerDiscoveryUseCase(
    private val meshService: MeshForegroundService,
    private val identityManager: SecureIdentityStateManager
) {
    suspend fun startDiscovery() {
        val myId = identityManager.currentIdentity()
        meshService.startScanning(myId)
    }
}

Implemented in the domain layer, this use-case calls the concrete MeshForegroundService (data layer) only through its public API contract, maintaining the architectural boundary.

Data-Layer Repository Implementation

class NostrRelayRepositoryImpl(
    private val httpClient: OkHttpClient
) : NostrRelayRepository {

    override suspend fun fetchRelays(): List<NostrRelay> {
        // Network request using OkHttp (platform code)
    }
}

This class resides in the data package and satisfies a domain interface, isolating OkHttp dependencies from business logic.

Summary

  • Clean Architecture in Bitchat Android strictly separates concerns into four layers: Presentation (com.bitchat.android.ui), Domain (com.bitchat.android.nostr, com.bitchat.android.identity), Data (com.bitchat.android.service, com.bitchat.android.mesh), and Platform (info.guardianproject.arti).
  • Dependencies always point inward, ensuring business logic in SecureIdentityStateManager and NostrRelayManager remains independent of Android framework code and external libraries like OkHttp or Arti.
  • Testability is achieved by isolating pure Kotlin domain classes that can be unit-tested without Android instrumentation or mocking complex system services.
  • Key files like MainViewModel.kt, MeshForegroundService.kt, and ArtiNative.kt demonstrate clear boundaries between UI state management, infrastructure concerns, and low-level native implementations.

Frequently Asked Questions

What are the four layers in Bitchat Android's Clean Architecture?

The architecture consists of the Presentation layer (UI components and ViewModels), Domain layer (pure Kotlin business logic), Data layer (infrastructure and external system implementations), and Platform layer (low-level native code and JNI bridges). This structure ensures that dependencies only flow inward toward the domain core, keeping business rules isolated from framework details.

How does Bitchat Android maintain testability with Clean Architecture?

By isolating business logic in pure Kotlin classes within the domain layer, such as SecureIdentityStateManager and PeerDiscoveryUseCase, the codebase allows for unit testing without Android framework dependencies. The separation of interfaces and implementations—where domain code depends on abstractions like NostrRelayRepository rather than concrete NostrRelayRepositoryImpl—enables mocking of data layer dependencies during domain testing.

Where is the dependency direction enforced in the codebase?

Dependency direction is enforced through package structure and import constraints: files in com.bitchat.android.ui import domain classes from com.bitchat.android.nostr, but never import platform-specific code from info.guardianproject.arti or Android service implementations directly. This creates a compile-time boundary that prevents layer violations, ensuring that MainViewModel cannot accidentally reference ArtiNative or OkHttp client implementations.

How are background services integrated without breaking Clean Architecture?

Services like MeshForegroundService implement interfaces that the domain layer can reference, and they expose functionality through coroutine flows or façade patterns. This allows ViewModels in the presentation layer to consume mesh networking capabilities—such as peer discovery and message relay—while maintaining strict separation between UI code and platform-specific background service logic that manages BLE advertisements or Tor circuit management.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →