Memilih arsitektur aplikasi adalah keputusan yang menentukan kecepatan pengembangan awal dan kelangsungan proyek jangka panjang. Banyak tim terjebak di dua kutub: memaksakan arsitektur kompleks untuk proyek sederhana, atau membiarkan kode berantakan karena terlalu sederhana. Pada chapter ini kita akan membangun decision tree sederhana yang memilih arsitektur berdasarkan skala proyek, skala tim, harapan pertumbuhan, dan jumlah integrasi eksternal. Pembaca juga akan melihat evolusi alami arsitektur sebuah produk dari MVP tunggal hingga sistem hexagonal dengan banyak integrasi.
Empat Pilihan Arsitektur #
| Arsitektur | Cocok untuk | Karakteristik |
|---|---|---|
| Script tunggal | Prototype, CLI sederhana, satu file main.kt | Cepat, sedikit boilerplate, sulit di-skala |
| Layered | Aplikasi menengah, CRUD dengan tim kecil | Pemisahan concern jelas, familiar |
| Clean | Domain kompleks tanpa banyak integrasi | Domain terisolasi, mudah diuji |
| Hexagonal | Domain kompleks + banyak integrasi | Ports and adapters, fleksibel |
Tidak ada arsitektur “terbaik” mutlak. Pilihan tergantung pada kombinasi skala, tim, dan ekspektasi pertumbuhan. Memulai sederhana dan berevolusi saat kebutuhan muncul biasanya lebih efektif daripada memaksakan arsitektur kompleks sejak hari pertama.
Decision Tree Sederhana #
Decision tree di atas adalah panduan praktis. Untuk implementasi konkret, kita bangun PemilihArsitektur yang mengkode aturan keputusan tersebut.
Implementasi Pemilih Arsitektur #
// Demo "Memilih Arsitektur Aplikasi": decision tree sederhana untuk memilih
// arsitektur berdasarkan kompleksitas domain dan tim.
import java.time.LocalDate
// === Decision tree: kategori proyek ===
enum class SkalaProyek { CRUD_SEDERHANA, APLIKASI_MENENGAH, DOMAIN_KOMPLEKS }
enum class SkalaTim { SOLO, KECIL, BESAR }
enum class Arsitektur { SCRIPT_TUNGGAL, LAYERED, CLEAN, HEXAGONAL }
data class KarakteristikProyek(
val skala: SkalaProyek,
val tim: SkalaTim,
val harapanTimKembang: Boolean, // apakah proyek akan berkembang signifikan
val banyakIntegrasi: Boolean // banyak integrasi eksternal
)
class PemilihArsitektur {
fun pilih(c: KarakteristikProyek): Pair<Arsitektur, String> {
return when {
c.skala == SkalaProyek.CRUD_SEDERHANA && c.tim == SkalaTim.SOLO && !c.harapanTimKembang ->
Arsitektur.SCRIPT_TUNGGAL to "CRUD sederhana, tim solo, no growth: cukup script/file tunggal."
c.skala == SkalaProyek.CRUD_SEDERHANA || c.skala == SkalaProyek.APLIKASI_MENENGAH ->
Arsitektur.LAYERED to "Aplikasi menengah atau CRUD dengan tim: Layered (presentation/application/infrastructure)."
c.skala == SkalaProyek.DOMAIN_KOMPLEKS && !c.banyakIntegrasi ->
Arsitektur.CLEAN to "Domain kompleks tanpa banyak integrasi: Clean Architecture (entities/use cases/adapters)."
c.skala == SkalaProyek.DOMAIN_KOMPLEKS && c.banyakIntegrasi ->
Arsitektur.HEXAGONAL to "Domain kompleks + banyak integrasi: Hexagonal (ports and adapters)."
else ->
Arsitektur.LAYERED to "Default aman: Layered."
}
}
}
// === Trade-off table (printed) ===
fun cetakTradeOff() {
println("=== Trade-off Arsitektur ===")
val rows = listOf(
Triple(Arsitektur.SCRIPT_TUNGGAL, "Kecepatan awal tinggi; sedikit boilerplate", "Sulit di-skala; logic bercampur"),
Triple(Arsitektur.LAYERED, "Pemisahan concern jelas; familiar", "Layer dapat bocor; tidak cukup untuk domain kompleks"),
Triple(Arsitektur.CLEAN, "Domain terisolasi; mudah diuji", "Boilerplate banyak; belokan curam"),
Triple(Arsitektur.HEXAGONAL, "Domain + integrasi terisolasi; mudah ganti adapter", "Setup paling kompleks; overkill untuk CRUD")
)
println("%-15s %-45s %-45s".format("Arsitektur", "Kelebihan", "Kekurangan"))
println("-".repeat(105))
rows.forEach { (a, p, k) ->
println("%-15s %-45s %-45s".format(a.name, p, k))
}
}
// === Evolusi arsitektur: mulai sederhana, evolve bila perlu ===
data class StatusProyek(
val nama: String,
val arsitekturSaatIni: Arsitektur,
val alasanEvolve: String
)
fun demoEvolusi() {
println()
println("=== Evolusi Arsitektur ===")
val timeline = listOf(
StatusProyek("MVP v0.1", Arsitektur.SCRIPT_TUNGGAL, "Awal: single file main.kt untuk validasi ide"),
StatusProyek("MVP v0.5", Arsitektur.LAYERED, "Tambah web UI + DB; pisahkan controller/service/repo"),
StatusProyek("v1.0", Arsitektur.LAYERED, "Produksi pertama; layered cukup"),
StatusProyek("v2.0", Arsitektur.CLEAN, "Domain mulai kompleks; tambah use case layer dan port"),
StatusProyek("v3.0", Arsitektur.HEXAGONAL, "Integrasi payment + shipping + email; refactor ke ports/adapters")
)
timeline.forEach { println(" ${it.nama}: ${it.arsitekturSaatIni} (${it.alasanEvolve})") }
}
fun main() {
println("=== Demo Memilih Arsitektur Aplikasi ===")
val pemilih = PemilihArsitektur()
val skenario = listOf(
KarakteristikProyek(SkalaProyek.CRUD_SEDERHANA, SkalaTim.SOLO, false, false),
KarakteristikProyek(SkalaProyek.APLIKASI_MENENGAH, SkalaTim.KECIL, true, false),
KarakteristikProyek(SkalaProyek.DOMAIN_KOMPLEKS, SkalaTim.KECIL, true, false),
KarakteristikProyek(SkalaProyek.DOMAIN_KOMPLEKS, SkalaTim.BESAR, true, true)
)
skenario.forEachIndexed { i, c ->
val (arsitektur, alasan) = pemilih.pilih(c)
println("Skenario ${i + 1}: $c")
println(" -> $arsitektur: $alasan")
}
println()
cetakTradeOff()
demoEvolusi()
}Output yang Dihasilkan:
=== Demo Memilih Arsitektur Aplikasi ===
Skenario 1: KarakteristikProyek(skala=CRUD_SEDERHANA, tim=SOLO, harapanTimKembang=false, banyakIntegrasi=false)
-> SCRIPT_TUNGGAL: CRUD sederhana, tim solo, no growth: cukup script/file tunggal.
Skenario 2: KarakteristikProyek(skala=APLIKASI_MENENGAH, tim=KECIL, harapanTimKembang=true, banyakIntegrasi=false)
-> LAYERED: Aplikasi menengah atau CRUD dengan tim: Layered (presentation/application/infrastructure).
Skenario 3: KarakteristikProyek(skala=DOMAIN_KOMPLEKS, tim=KECIL, harapanTimKembang=true, banyakIntegrasi=false)
-> CLEAN: Domain kompleks tanpa banyak integrasi: Clean Architecture (entities/use cases/adapters).
Skenario 4: KarakteristikProyek(skala=DOMAIN_KOMPLEKS, tim=BESAR, harapanTimKembang=true, banyakIntegrasi=true)
-> HEXAGONAL: Domain kompleks + banyak integrasi: Hexagonal (ports and adapters).
=== Trade-off Arsitektur ===
Arsitektur Kelebihan Kekurangan
---------------------------------------------------------------------------------------------------------
SCRIPT_TUNGGAL Kecepatan awal tinggi; sedikit boilerplate Sulit di-skala; logic bercampur
LAYERED Pemisahan concern jelas; familiar Layer dapat bocor; tidak cukup untuk domain kompleks
CLEAN Domain terisolasi; mudah diuji Boilerplate banyak; belokan curam
HEXAGONAL Domain + integrasi terisolasi; mudah ganti adapter Setup paling kompleks; overkill untuk CRUD
=== Evolusi Arsitektur ===
MVP v0.1: SCRIPT_TUNGGAL (Awal: single file main.kt untuk validasi ide)
MVP v0.5: LAYERED (Tambah web UI + DB; pisahkan controller/service/repo)
v1.0: LAYERED (Produksi pertama; layered cukup)
v2.0: CLEAN (Domain mulai kompleks; tambah use case layer dan port)
v3.0: HEXAGONAL (Integrasi payment + shipping + email; refactor ke ports/adapters)Bedah Kode Baris-per-Baris #
Mari kita amati bagian kunci:
- Baris 9-12 (enum): Empat enum merepresentasikan dimensi keputusan.
SkalaProyekmengukur kompleksitas domain;SkalaTimmengukur ukuran tim;Arsitekturadalah hasil keputusan. - Baris 14-18 (
KarakteristikProyek): Data class yang merangkum empat dimensi input.harapanTimKembangpenting karena proyek sederhana yang akan tumbuh sebaiknya tidak memakai script tunggal meskipun saat ini masih kecil. - Baris 20-36 (
PemilihArsitektur.pilih): Decision tree yang dikode sebagaiwhenberurutan. Urutan cabang penting: kasus paling spesifik dicek dulu. Default aman ke Layered. - Baris 39-49 (
cetakTradeOff): Fungsi yang mencetak tabel trade-off. Format%-15smemakaiString.formatuntuk alignment kolom. - Baris 60-69 (evolusi): Timeline perkembangan produk hipotetis dari MVP v0.1 hingga v3.0. Setiap versi mengganti arsitektur karena dorongan nyata: tambah UI, kompleksitas domain, atau banyak integrasi.
Evolusi Alami Arsitektur #
Memulai dengan script tunggal lalu berevolusi adalah pola yang umum. Banyak produk sukses memulai sebagai satu file main.kt, lalu terurai menjadi layered saat web UI ditambah, menjadi clean saat domain kompleks, dan menjadi hexagonal saat banyak integrasi muncul. Kunci evolusi: refaktor dilakukan karena kebutuhan nyata, bukan karena “kelihatannya lebih bagus”. Refaktor prematur menambah boilerplate tanpa manfaat.
Penutup #
Memilih arsitektur adalah trade-off antara kecepatan awal dan keberlanjutan jangka panjang. Decision tree sederhana berdasarkan skala, tim, pertumbuhan, dan integrasi membantu memilih titik awal. Evolusi alami dari script tunggal ke arsitektur kompleks adalah pola yang sehat selama didorong kebutuhan nyata. Setelah menyelesaikan section Software Design dan Architecture, kita akan beralih ke section Algorithms dan Data Structures: fondasi untuk menulis kode yang efisien dan terstruktur.
Referensi #
- The Clean Architecture — Robert C. Martin
- Hexagonal Architecture — Alistair Cockburn
- Kotlin Coding Conventions
Referensi Lanjutan #
- Kotlin documentation home — halaman utama dokumentasi Kotlin sebagai titik masuk memilih pendekatan arsitektur berbasis fitur bahasa