Clean Architecture — dasar, lapisan Entities, Use Cases dan Gateways

Penulis: IT Sectr Diterbitkan: 2026-02-17 Waktu membaca: 10 mnt

Clean Architecture — arsitektur berlapis yang diusulkan oleh Robert Martin (Uncle Bob) pada tahun 2012, membagi aplikasi menjadi lapisan independen: Domain (Entities, Use Cases), Data (Repositories, DataSources) dan Presentation (ViewModels, Views). Prinsip utama — Dependency Rule: dependensi diarahkan ke dalam, lapisan eksternal bergantung pada lapisan internal, bukan sebaliknya. Clean Architecture diterapkan dalam pengembangan mobile untuk proyek dengan kompleksitas logika bisnis yang tinggi. Selengkapnya — di buku The Clean Architecture.

Poin Utama

  • Clean Architecture — tiga lapisan: Domain (logika bisnis), Data (data), Presentation (UI) dengan Dependency Rule
  • Dependency Rule — dependensi diarahkan ke dalam, Domain tidak tahu tentang Data dan Presentation
  • Use Cases (Interactors) — skenario logika bisnis, setiap Use Case — satu kelas dengan satu metode
  • Repository Interface — abstraksi data di Domain, implementasi — di lapisan Data
  • Testabilitas — Domain dan Use Cases diuji dengan unit test tanpa Android SDK dan iOS UIKit

Clean Architecture — dasar arsitektur berlapis

Clean Architecture — pola arsitektur yang dirumuskan oleh Robert Martin (Uncle Bob) pada tahun 2012. Ide utama — pembagian aplikasi menjadi lapisan dengan aturan dependensi yang ketat: kode di dalam lapisan tidak tahu tentang kode di luar. Lapisan eksternal (UI, framework, database) — detail implementasi. Lapisan internal (logika bisnis, aturan perusahaan) — esensi aplikasi.

Lapisan Clean Architecture dalam pengembangan mobile: 1) Domain — Entities (objek bisnis) dan Use Cases (kasus penggunaan); 2) Data — RepositoryImpl (implementasi repositori), DataSources (jaringan, database, cache); 3) Presentation — ViewModels, Views (Compose/SwiftUI). Domain — lapisan paling dalam, tanpa dependensi. Data bergantung pada Domain (mengimplementasikan antarmuka repositori). Presentation bergantung pada Domain (memanggil Use Cases, berlangganan hasil).

LapisanBerisiDependensi
DomainEntities, Use Cases, Repository InterfacesTidak ada (Kotlin/Swift murni)
DataRepositoryImpl, DataSources (API, DB, Cache)Domain, Retrofit, Room, Ktor
PresentationViewModels, Views, ComposablesDomain, Jetpack, SwiftUI

Dependency Rule — satu-satunya aturan ketat Clean Architecture. Kode sumber hanya dapat merujuk ke lapisan di dalamnya atau ke lapisan di bawahnya (lebih dekat ke pusat). Presentation mengimpor Domain. Domain TIDAK mengimpor Data atau Presentation. Ini dicapai melalui pembalikan dependensi (Dependency Inversion Principle): Domain mendefinisikan antarmuka Repository, Data mengimplementasikannya. Presentation bergantung pada abstraksi UseCase, bukan pada repositori konkret.

Domain Layer: Entities, Use Cases dan Repository Interfaces

Domain — lapisan paling stabil dari aplikasi. Entities — objek bisnis tidak tergantung pada framework: User, Product, Order. Use Cases — kelas dengan satu metode invoke (atau operator fun invoke di Kotlin), mengimplementasikan satu skenario: GetUserUseCase, PlaceOrderUseCase, CalculateTotalUseCase. Repository Interfaces — abstraksi akses data, didefinisikan di Domain, diimplementasikan di Data. Domain tidak mengandung Android SDK, iOS UIKit, Retrofit, Room — hanya Kotlin atau Swift murni.

swift
// Entity — objek bisnis (Domain)
struct User: Equatable {
    let id: Int
    let name: String
    let email: String
}

// Repository Interface — abstraksi data (Domain)
protocol UserRepository {
    func getUser(id: Int) async throws -> User
    func getUsers() async throws -> [User]
}

// Use Case — satu skenario (Domain)
final class GetUserUseCase {
    private let repository: UserRepository

    init(repository: UserRepository) {
        self.repository = repository
    }

    func execute(id: Int) async throws -> User {
        return try await repository.getUser(id: id)
    }
}

Use Case — «kelas dengan satu metode» — bukan dogma, melainkan rekomendasi praktis. Ketika Use Case menjadi lebih kompleks (validasi + logging + panggilan repositori), metode-metodenya dikelompokkan berdasarkan arti: UserUseCase.getUser, UserUseCase.searchUsers, UserUseCase.deleteUser. Penting — Use Case tidak boleh tahu dari mana data berasal (jaringan, database, cache) dan siapa yang menampilkannya (Compose, SwiftUI). Di IT Sectr kami mengalokasikan Use Case untuk setiap operasi yang memiliki aturan bisnis, validasi, atau penggabungan data dari dua sumber.

Kemurnian Domain dicapai melalui pemetaan DTO di batas lapisan. Lapisan Data menerima model JSON (DTO), memetakannya ke Entity Domain. Presentation menerima Entity Domain, memetakannya ke ViewModel (DisplayItem). Entity Domain tidak pernah mengandung anotasi Retrofit, Room, Codable — ini menjamin bahwa lapisan tidak perlu diubah saat mengganti database dari Room ke Realm atau saat mengganti Retrofit dengan Ktor.

Data Layer: Repository Implementation dan DataSources

Data Layer — implementasi antarmuka yang didefinisikan di Domain. Berisi RepositoryImpl (kelas yang mengimplementasikan UserRepository) dan DataSources (RemoteDataSource — API, LocalDataSource — database, CacheDataSource — SharedPreferences/NSUserDefaults). Lapisan Data bergantung pada Domain (mengimpor antarmuka repositori dan Entities) dan pada framework (Retrofit, Room, Ktor, CoreData). RepositoryImpl menyembunyikan sumber data dari Domain — Use Case tidak tahu apakah data berasal dari jaringan atau cache.

kotlin
// DTO — model untuk jaringan (Data)
data class UserDto(
    @SerializedName("id") val id: Int,
    @SerializedName("first_name") val firstName: String,
    @SerializedName("last_name") val lastName: String,
    @SerializedName("email") val email: String
)

// RepositoryImpl — implementasi (Data)
class UserRepositoryImpl(
    private val remoteDataSource: UserRemoteDataSource,
    private val localDataSource: UserLocalDataSource
) : UserRepository {

    override suspend fun getUser(id: Int): User {
        // Mencoba mengambil dari cache
        localDataSource.getUser(id)?.let { return it.toDomain() }
        // Jika tidak ada — memuat dari jaringan
        val dto = remoteDataSource.fetchUser(id)
        val user = dto.toDomain()
        localDataSource.saveUser(user)
        return user
    }

    override suspend fun getUsers(): List<User> {
        return remoteDataSource.fetchAllUsers().map { it.toDomain() }
    }
}

// Mapper — konversi DTO ↔ Domain
fun UserDto.toDomain() = User(
    id = id,
    name = "$firstName $lastName",
    email = email
)

Strategi caching di Data Layer: RepositoryImpl pertama memeriksa penyimpanan lokal, jika tidak ada data — memuat dari jaringan dan menyimpan secara lokal. Jika jaringan tidak tersedia — mengembalikan data usang dengan tanda isStale. Use Case di Domain tidak tahu tentang strategi — menerima User melalui Repository.getUser(id). Perubahan strategi (misalnya, invalidasi cache setiap 15 menit) tidak mempengaruhi Domain dan Presentation.

Modularitas di Android — Kotlin Multiplatform memungkinkan Domain dipindahkan ke modul KMP terpisah tanpa dependensi Android SDK. Data — modul terpisah dengan dependensi pada Domain. Presentation — modul Android dengan dependensi pada Domain. Dependensi Gradle: domain (pure Kotlin), data (domain + Retrofit + Room), app (domain + presentation + Hilt). Modularitas seperti ini wajib untuk proyek besar — CI membangun Domain secara terpisah, unit test Domain tidak memerlukan emulator Android.

Presentation Layer: ViewModels dan Views

Presentation Layer — lapisan terluar dari Clean Architecture. Berisi ViewModels (Android) / ObservableObject (iOS) dan Views (Compose/SwiftUI). ViewModel memanggil Use Case, menerima hasil dan mengubahnya menjadi status UI (State). View berlangganan State dan menampilkan. Presentation bergantung pada Domain — mengimpor Use Cases dan Entities. Presentation tidak mengimpor Data Layer — data datang melalui Use Case, yang secara internal menggunakan Repository.

ViewModel di Clean Architecture tidak mengandung logika bisnis — memanggil Use Case. Jika Use Case mengembalikan User, ViewModel mengubahnya menjadi UserDisplayItem (name, emailFormatted, avatarUrl) — model presentasi murni. Use Case tidak tahu tentang DisplayItem — mengembalikan Entity. Pemisahan ini memungkinkan pengujian Use Case tanpa UI dan ViewModel tanpa UseCase (melalui mock). Di IT Sectr kami secara ketat mematuhi: Use Case — logika bisnis, ViewModel — hanya presentasi, View — hanya tampilan.

kotlin
// Use Case (Domain) — logika bisnis murni
class GetUserUseCase(
    private val repository: UserRepository
) {
    suspend operator fun invoke(id: Int): User {
        return repository.getUser(id)
    }
}

// ViewModel (Presentation) — hanya presentasi
class UserViewModel(
    private val getUserUseCase: GetUserUseCase
) : ViewModel() {

    private val _state = MutableStateFlow<UserScreenState>(UserScreenState.Loading)
    val state: StateFlow<UserScreenState> = _state.asStateFlow()

    fun loadUser(id: Int) {
        viewModelScope.launch {
            _state.value = UserScreenState.Loading
            val user = getUserUseCase(id)
            val displayItem = UserDisplayItem(
                name = user.name,
                email = user.email,
                initials = user.name.split(" ").joinToString("") { it.first().toString() }
            )
            _state.value = UserScreenState.Success(displayItem)
        }
    }
}

data class UserDisplayItem(
    val name: String,
    val email: String,
    val initials: String
)

sealed interface UserScreenState {
    data object Loading : UserScreenState
    data class Success(val displayItem: UserDisplayItem) : UserScreenState
    data class Error(val message: String) : UserScreenState
}

Navigation di lapisan Presentation — bagian dari cincin luar. Clean Architecture tidak menentukan mekanisme navigasi — bisa NavController (Compose), NavigationStack (SwiftUI), Coordinator (UIKit) atau Router (VIPER). Penting: keputusan navigasi dibuat oleh Presentation, tetapi navigasi tidak boleh menembus ke Use Case. Use Case mengembalikan hasil, ViewModel memutuskan ke layar mana akan pergi. Di Clean Architecture navigasi adalah detail yang dapat diganti tanpa mengubah Domain.

Clean Architecture di iOS dan Android: contoh kode

Clean Architecture di Android diimplementasikan melalui modul Gradle: domain (pure Kotlin), data (domain + Retrofit + Room), presentation (domain + Compose). Struktur folder: domain/user/User.kt, GetUserUseCase.kt, UserRepository.kt; data/remote/UserRemoteDataSource.kt, local/UserDao.kt, repository/UserRepositoryImpl.kt; presentation/ui/user/UserViewModel.kt, UserScreen.kt. DI (Hilt) menghubungkan lapisan: UserRepositoryImpl diikat ke antarmuka UserRepository di modul domain.

Clean Architecture di iOS menggunakan SPM atau grup Xcode tanpa modul terpisah (karena keterbatasan Xcode). Domain — folder dengan file yang tidak mengimpor UIKit atau SwiftUI. Data — folder dengan APIClient, CoreDataStack, RepositoryImpl. Presentation — folder dengan ViewModels dan SwiftUI Views. DI melalui konstruktor atau assembly di App. Panggilan utama — async/await melalui UseCase.execute() dengan pemeriksaan MainActor untuk pembaruan UI.

swift
// Data Layer: Remote DataSource (iOS)
final class UserRemoteDataSource {
    private let apiClient: APIClient

    func fetchUser(id: Int) async throws -> UserDTO {
        return try await apiClient.get("/users/\(id)")
    }
}

// Repository Implementation (Data)
final class UserRepositoryImpl: UserRepository {
    private let remote: UserRemoteDataSource
    private let local: UserLocalDataSource

    func getUser(id: Int) async throws -> User {
        if let cached = try await local.getUser(id) {
            return cached
        }
        let dto = try await remote.fetchUser(id)
        let user = dto.toDomain()
        try await local.saveUser(user)
        return user
    }
}

// Presentation: ViewModel + SwiftUI View
@MainActor
final class UserViewModel: ObservableObject {
    @Published private(set) var state: UserScreenState = .loading
    private let getUserUseCase: GetUserUseCase

    func loadUser(id: Int) {
        Task {
            state = .loading
            if let user = try? await getUserUseCase.execute(id: id) {
                state = .success(user)
            } else {
                state = .error("Failed to load")
            }
        }
    }
}

Clean Architecture di proyek IT Sectr — standar kami untuk proyek mulai 30 hari. Kami menggunakan arsitektur tiga lapis dengan Kotlin Multiplatform untuk Android/iOS sejak 2022. Domain — modul KMP bersama, Data — modul platform (Retrofit di Android, URLSession di iOS), Presentation — UI native. Ini memberikan 60–80% kode bersama logika bisnis antara iOS dan Android, mengurangi waktu pengembangan 30–40% dibandingkan dengan dua implementasi terpisah.

Pertanyaan Umum

Berapa banyak lapisan yang harus ada di Clean Architecture?

Minimal tiga: Domain, Data, Presentation. Untuk proyek besar ditambahkan Framework (dependensi Android SDK/iOS UIKit) dan Device (GPS, kamera, sensor). Jumlah lapisan — bukan aturan ketat, masalah kenyamanan. Yang penting adalah mematuhi Dependency Rule: dependensi diarahkan ke dalam, ke Domain. Bisa mulai dengan tiga dan menambah lapisan seiring pertumbuhan proyek.

Apakah Clean Architecture meningkatkan jumlah kode?

Ya — 30–50% dibandingkan MVVM karena pemisahan antarmuka repositori, Use Cases dan mapper. Untuk aplikasi CRUD sederhana ini berlebihan. Clean Architecture dibenarkan untuk proyek dengan logika bisnis kompleks, di mana testabilitas dan isolasi lapisan lebih penting daripada kecepatan pengembangan. Untuk MVP atau prototipe gunakan MVVM — Clean Architecture akan memperlambat peluncuran.

Bisakah Clean Architecture dikombinasikan dengan MVI?

Ya, ini adalah praktik umum. Use Cases tetap di Domain dan Presentation menggunakan siklus MVI (Intent → Reducer → State). Data Layer — sama, Domain — sama. MVI di Presentation memberikan status layar yang dapat diprediksi, Clean Architecture — isolasi logika bisnis. Kombinasi ini digunakan dalam proyek besar dengan puluhan pengembang.

Apakah Use Cases diperlukan untuk setiap permintaan data?

Use Case diperlukan ketika operasi mencakup aturan bisnis: validasi, penggabungan data dari dua sumber, perhitungan, logging, pemeriksaan hak akses. Permintaan getUser(id) sederhana tanpa logika tambahan dapat memanggil Repository langsung dari ViewModel. Namun untuk keseragaman arsitektur, banyak tim membuat Use Case untuk setiap metode publik Repository — ini menambah 5–10% kode tetapi menyederhanakan pembacaan.

Bagaimana cara menguji Clean Architecture?

Domain: unit test Use Cases dengan mock Repository — Kotlin/Swift murni tanpa Android SDK. Data: tes integrasi RepositoryImpl dengan mock/fake DataSource. Presentation: tes ViewModel dengan mock UseCase. Berkat Dependency Rule setiap lapisan diuji secara terisolasi. Di IT Sectr cakupan Domain mencapai 95%, Data — 70–80%, Presentation — 60–70%.

Ringkasan

  • Clean Architecture — tiga lapisan (Domain, Data, Presentation) dengan Dependency Rule ke dalam
  • Dependency Rule — Domain tidak tahu tentang Data dan Presentation, isolasi melalui antarmuka
  • Domain — Entities, Use Cases, Repository Interfaces — Kotlin/Swift murni tanpa framework
  • Data — RepositoryImpl, DataSources (jaringan, database, cache) — implementasi antarmuka Domain
  • Presentation — ViewModels, Views — hanya tampilan, logika bisnis di Use Cases
  • Pengujian — Domain tercakup unit test 90–95%
  • KMP — Clean Architecture dengan Kotlin Multiplatform memberikan 60–80% kode bersama iOS + Android

Kami akan mengembangkan aplikasi seluler turnkey

IT Sectr membuat aplikasi iOS dan Android untuk startup dan bisnis sejak 2017. Kami akan memberi saran dan mengusulkan solusi terbaik.

Diskusikan proyek

Baca juga