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 #

ArsitekturCocok untukKarakteristik
Script tunggalPrototype, CLI sederhana, satu file main.ktCepat, sedikit boilerplate, sulit di-skala
LayeredAplikasi menengah, CRUD dengan tim kecilPemisahan concern jelas, familiar
CleanDomain kompleks tanpa banyak integrasiDomain terisolasi, mudah diuji
HexagonalDomain kompleks + banyak integrasiPorts 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 #

kotlin
// 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:

text
=== 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:

  1. Baris 9-12 (enum): Empat enum merepresentasikan dimensi keputusan. SkalaProyek mengukur kompleksitas domain; SkalaTim mengukur ukuran tim; Arsitektur adalah hasil keputusan.
  2. Baris 14-18 (KarakteristikProyek): Data class yang merangkum empat dimensi input. harapanTimKembang penting karena proyek sederhana yang akan tumbuh sebaiknya tidak memakai script tunggal meskipun saat ini masih kecil.
  3. Baris 20-36 (PemilihArsitektur.pilih): Decision tree yang dikode sebagai when berurutan. Urutan cabang penting: kasus paling spesifik dicek dulu. Default aman ke Layered.
  4. Baris 39-49 (cetakTradeOff): Fungsi yang mencetak tabel trade-off. Format %-15s memakai String.format untuk alignment kolom.
  5. 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 #

Referensi Lanjutan #

  • Kotlin documentation home — halaman utama dokumentasi Kotlin sebagai titik masuk memilih pendekatan arsitektur berbasis fitur bahasa