Layered architecture membagi aplikasi menjadi beberapa layer horizontal yang masing-masing memiliki tanggung jawab berbeda. Pola ini menjadi fondasi paling umum untuk aplikasi enterprise karena sederhana, familiar, dan memisahkan concern dengan jelas. Pada chapter ini kita akan membangun aplikasi checkout sederhana dengan empat layer: domain (aturan bisnis), application (use case), infrastructure (implementasi konkret), dan presentation (CLI). Pembaca akan melihat bagaimana dependensi mengalir dari luar ke dalam, sementara domain tetap murni tanpa menyentuh detail infrastruktur.

Empat Layer dan Arah Dependensi #

Layered architecture klasik membagi aplikasi menjadi empat lapisan:

LayerTanggung jawabBoleleh bergantung ke
PresentationMenerima input, menampilkan outputApplication
ApplicationMengorkestrasi use case, transaksiDomain (interface)
DomainAturan bisnis inti, entity, value objectTidak ada (paling dalam)
InfrastructureImplementasi konkret: DB, API, fileDomain (interface)

Domain layer tidak tahu tentang database, HTTP, atau framework. Ia hanya mendefinisikan interface yang dibutuhkan, sementara infrastructure menyediakan implementasi konkret. Arah dependensi selalu dari luar ke dalam; tidak boleh sebaliknya.

Implementasi Layered Checkout #

Contoh berikut membangun aplikasi checkout yang menghitung total keranjang, memproses pembayaran, dan mengirim notifikasi. Setiap layer terpisah jelas melalui package atau class.

kotlin
// Demo layered architecture: presentation, application, domain, infrastructure.

// === Domain layer: aturan bisnis inti, tidak bergantung layer lain ===
data class Produk(val id: Int, val nama: String, val harga: Double)
data class Keranjang(val items: MutableList<Pair<Produk, Int>> = mutableListOf()) {
    fun tambah(p: Produk, jml: Int) = items.add(p to jml)
    fun total(): Double = items.sumOf { (p, j) -> p.harga * j }
}

// === Application layer: mengorkestrasi use case ===
class CheckoutUseCase(
    private val repo: ProdukRepository,        // interface dari domain
    private val payment: PaymentGateway,        // interface dari domain
    private val notifier: Notifier              // interface dari domain
) {
    data class Hasil(val total: Double, val pembayaran: String, val notifikasi: String)

    fun jalankan(produkIds: List<Pair<Int, Int>>): Hasil {
        val keranjang = Keranjang()
        for ((id, jml) in produkIds) {
            val p = repo.cari(id) ?: throw IllegalArgumentException("Produk $id tidak ada")
            keranjang.tambah(p, jml)
        }
        val total = keranjang.total()
        val trxId = payment.bayar(total)
        val notif = notifier.kirim("Pesanan dibayar: Rp$total")
        return Hasil(total, trxId, notif)
    }
}

// === Domain interfaces (ports) ===
interface ProdukRepository { fun cari(id: Int): Produk? }
interface PaymentGateway { fun bayar(jumlah: Double): String }
interface Notifier { fun kirim(pesan: String): String }

// === Infrastructure layer: implementasi konkret ===
class InMemoryProdukRepository(private val data: Map<Int, Produk>) : ProdukRepository {
    override fun cari(id: Int): Produk? = data[id]
}

class StripePaymentGateway : PaymentGateway {
    private var counter = 0
    override fun bayar(jumlah: Double): String {
        counter++
        return "TRX-$counter-Rp$jumlah"
    }
}

class EmailNotifier : Notifier {
    override fun kirim(pesan: String) = "Email dikirim: $pesan"
}

// === Presentation layer: user interface ===
class CliCheckout(private val useCase: CheckoutUseCase) {
    fun proses(input: String) {
        val pairs = input.split(",").map {
            val (id, jml) = it.split(":").let { p -> p[0].toInt() to p[1].toInt() }
            id to jml
        }
        val hasil = useCase.jalankan(pairs)
        println("=== Hasil Checkout ===")
        println("Total: Rp${hasil.total}")
        println("Pembayaran: ${hasil.pembayaran}")
        println("Notifikasi: ${hasil.notifikasi}")
    }
}

fun main() {
    println("=== Demo Layered Architecture ===")

    val repo = InMemoryProdukRepository(mapOf(
        1 to Produk(1, "Buku", 15_000.0),
        2 to Produk(2, "Pulpen", 5_000.0),
        3 to Produk(3, "Penggaris", 3_000.0)
    ))
    val payment = StripePaymentGateway()
    val notifier = EmailNotifier()
    val useCase = CheckoutUseCase(repo, payment, notifier)

    val cli = CliCheckout(useCase)
    cli.proses("1:2,2:3,3:1")
}

Output yang Dihasilkan:

text
=== Demo Layered Architecture ===
=== Hasil Checkout ===
Total: Rp48000.0
Pembayaran: TRX-1-Rp48000.0
Notifikasi: Email dikirim: Pesanan dibayar: Rp48000.0

Bedah Kode Baris-per-Baris #

Mari kita amati bagian kunci:

  1. Baris 4-9 (Produk, Keranjang): Domain layer berisi model bisnis inti. Keranjang.tambah dan Keranjang.total adalah aturan bisnis yang tidak peduli dengan database atau UI.
  2. Baris 11-29 (CheckoutUseCase): Application layer mengorkestrasi use case. Ia menerima tiga interface sebagai dependensi, lalu memanggil method pada masing-masing interface tanpa peduli implementasi konkret.
  3. Baris 32-34 (interface ports): Domain interface sering disebut “port”. Application layer memanggil port, sementara infrastructure menyediakan “adapter” yang mengimplementasinya.
  4. Baris 37-51 (infrastructure konkret): Tiga class InMemoryProdukRepository, StripePaymentGateway, dan EmailNotifier adalah adapter konkret. Mengganti StripePaymentGateway ke MidtransGateway cukup dilakukan di composition root tanpa mengubah use case.
  5. Baris 54-66 (CliCheckout): Presentation layer mengonversi input string "1:2,2:3" menjadi list of pair. Ia hanya tahu tentang use case, tidak tahu tentang repository atau payment gateway.

Jebakan Layer Bocor #

Layered architecture yang diterapkan tanpa disiplin sering menjadi “layered cake” yang bocor: presentation memanggil infrastructure langsung, atau domain memegang tipe dari framework. Gejala ini biasanya muncul saat deadline mepet dan developer mengambil jalan pintas melewatkan application layer. Akibatnya layer tidak bisa diganti secara independen dan tujuan awal pemisahan gagal.

Penutup #

Layered architecture cocok untuk aplikasi menengah yang punya domain relatif lurus. Setiap layer dapat diuji secara independen, dan mengganti komponen infrastructure tidak menyentuh kode bisnis. Pada chapter berikutnya kita akan naik ke Clean Architecture: pendekatan yang lebih ketat dengan dependency rule yang eksplisit dan entities layer terpisah dari use cases.

Referensi #

Referensi Lanjutan #

  • Kotlin documentation home — halaman utama dokumentasi Kotlin untuk memetakan package per lapisan: domain, application, infrastructure