Clean Architecture
On this page
- Sekilas
- Gagasannya secara singkat
- A. Tujuannya: perubahan yang murah
- B. Dependency Rule
- C. Lingkaran-lingkarannya
- D. Menyeberangi batas: port dan adapter
- Kenapa saya memilihnya
- Kesepakatan saya
- Cara saya memeriksa module
- Di mana saya tidak memakainya
- A. Antarmuka web berbasis React atau Vue
- B. Tool satu file
- C. Versi cepat pertama service Rust
- D. Project terkunci framework dengan tim yang sudah ada
- E. Batas yang tak membayar dirinya sendiri
- Rujukan
- Istilah
Clean Architecture adalah cara menyusun software supaya aturan yang menjadi alasan program itu ada tak pernah bergantung pada tool yang menjalankannya. Gagasan dan kosakatanya berasal dari Robert C. Martin, dalam Clean Architecture: A Craftsman's Guide to Software Structure and Design (2017).
Ini salah satu arsitektur yang saya pakai, bukan satu-satunya. Saya memakainya ketika membangun service atau aplikasi yang aturannya semestinya bertahan lebih lama daripada database, framework, dan storage-nya. Halaman ini menjelaskan kenapa saya memilihnya, kesepakatan yang saya ikuti di mana pun saya memakainya, cara saya memeriksa module terhadapnya, dan tempat-tempat yang sengaja saya tangani dengan arsitektur lain.
Sekilas
| Asal | Robert C. Martin, Clean Architecture (2017) |
|---|---|
| Saya pakai untuk | Backend service dan aplikasi yang aturannya semestinya bertahan lebih lama daripada tool-nya, termasuk aplikasi Flutter |
| Tak saya pakai untuk | Antarmuka web berbasis React atau Vue, tool satu file, dan project yang terkunci framework seperti Laravel |
| Bahasa | Rust, Python, Dart (Flutter), Node.js, C |
Gagasannya secara singkat
A. Tujuannya: perubahan yang murah
Martin mendefinisikan tujuan arsitektur software sebagai menekan tenaga manusia yang dibutuhkan untuk membangun dan merawat sistem. Arsitektur yang baik membuat ongkos satu perubahan kira-kira sepadan dengan nilai perubahannya; arsitektur yang buruk mengubah perubahan kecil menjadi pekerjaan berminggu-minggu, karena efeknya merambat ke kode yang semestinya tak ikut terlibat.
Alat utamanya adalah pembedaan antara policy dan detail. Policy adalah tujuan sistem itu ada: aturan bisnis, yang tetap sama entah datanya disimpan di PostgreSQL atau di file biasa. Detail adalah mekanisme yang menjalankan policy: database, web framework, protokol HTTP, client storage. Detail memang diperlukan, tapi justru bagian yang paling mungkin berubah.
B. Dependency Rule
Semuanya bertumpu pada satu aturan: dependency di source code hanya menunjuk ke dalam, dari detail ke policy. Kode di lapisan dalam tak pernah menyebut nama yang dideklarasikan di lapisan luar. Lapisan dalam tak tahu database mana yang ada, framework mana yang melayaninya, atau bagaimana hasilnya akan ditampilkan. Karena itu mengganti tool hanya menyentuh lapisan luar, dan arah yang membuat software mahal — aturan berubah gara-gara tool berubah — tak mungkin terjadi.
C. Lingkaran-lingkarannya
Martin menggambar arsitektur ini sebagai lingkaran-lingkaran sepusat dengan policy di tengahnya:
- Entities memegang aturan inti domain, aturan yang tetap benar seandainya aplikasi di sekelilingnya ditulis ulang.
- Use cases memegang aturan khusus aplikasi: apa yang terjadi, dan dalam urutan apa, ketika seseorang meminta sistem mengerjakan sesuatu.
- Interface adapters menerjemahkan antara bentuk yang disukai use case dan bentuk yang dibutuhkan dunia luar — request, response, baris tabel, halaman yang dirender.
- Frameworks and drivers adalah tool-nya sendiri: database, web server, client storage, message broker.
Martin menegaskan bahwa angka empat bukan hukum. Yang wajib dipegang adalah arah panahnya, bukan jumlah lingkarannya.
D. Menyeberangi batas: port dan adapter
Use case sering harus menjangkau sesuatu di luar, misalnya menyimpan record. Use case tak bisa langsung memanggil database tanpa melanggar aturan, jadi ia mendeklarasikan interface yang menggambarkan kebutuhannya, dan lapisan luar menyediakan implementasinya. Interface itu milik lapisan dalam; implementasinya tinggal di luar dan menunjuk ke dalam. Interface ini saya sebut port, dan implementasinya adapter.
Kenapa saya memilihnya
Tiap project saya punya peta yang sama. Inilah alasan yang paling saya hargai. Entah project-nya ditulis dengan Rust, Python, Dart, Node.js, atau C, empat folder yang sama berdiri di puncak source tree-nya, dengan arti yang sama. Ketika saya membuka project yang berbulan-bulan tak saya sentuh, atau project berbahasa lain dari yang saya kerjakan kemarin, saya sudah tahu letak use case, letak kode database, dan pintu masuk request. Berpindah bahasa cuma membuat saya membayar sintaksnya, bukan orientasinya. Peta yang sama juga menolong siapa pun yang membaca kode sesudah saya, termasuk AI agent yang saya izinkan menelusuri repository: use case ditemukan lewat namanya, dan folder-nya sudah mengatakan bahwa isinya bebas SQL dan bebas HTTP.
Tool-nya berganti, aturannya tak perlu ikut berganti. Dari tahun ke tahun saya mengganti database, pipeline logging, storage server, dan web framework. Karena use case hanya mengenal port, menukar tool di balik satu port berarti menulis adapter baru dan mengubah baris di main component yang memilihnya. Aturannya sendiri tak tersentuh, dan justru itulah gunanya memisahkan keduanya.
Aturannya bisa diuji tanpa tool-nya. Use case yang bergantung pada port bisa diberi adapter in-memory saat test. Itu membuat pengujian tiap use case jadi murah — tanpa database yang harus dinyalakan, tanpa storage server yang harus dijangkau — sehingga yang saya uji adalah logika yang penting, bukan cuma logika yang gampang dijangkau.
Bukunya memberi saya checklist, bukan sekadar gambar. Bab-bab Martin cukup konkret untuk diubah jadi pertanyaan yang bisa saya ajukan kepada module yang sudah selesai. Dua seksi berikutnya adalah hasilnya: kesepakatan yang saya terapkan, dan pertanyaan yang saya pakai untuk memeriksanya.
Kesepakatan saya
Inilah kesepakatan yang diikuti module saya ketika halamannya menyatakan memakai Clean Architecture. Nama folder-nya sama di semua bahasa:
| Lingkaran | Folder | Isinya |
|---|---|---|
| Entities | domain/ | entities, value objects, error domain |
| Use cases | application/ | use case, port yang dibutuhkannya, tipe input, validasi |
| Interface adapters | interface/ | request handler, presenter, penerjemahan error ke response |
| Frameworks and drivers | infrastructure/ | adapter untuk database, storage, telemetry, konfigurasi |
Dua aturan menjaga peta itu tetap jujur. shared/ hanya berisi helper yang bukan milik lingkaran mana pun, seperti utilitas encoding atau waktu, dan tak boleh jadi tempat menyembunyikan logika bisnis. Lalu entry point adalah satu-satunya file yang tahu adapter mana berdiri di balik tiap port; Martin menyebutnya main component, bagian paling kotor dari sistem yang sengaja dijaga tetap kecil.
Port dideklarasikan oleh use case yang membutuhkannya, dan tak mengatakan apa pun tentang cara kebutuhan itu dipenuhi:
// application/ports: yang dibutuhkan use case
// infrastructure/persistence: salah satu cara menyediakannya
Bentuk yang sama berlaku di semua bahasa yang saya pakai; yang berubah cuma sebutan untuk "kemampuan tanpa implementasi":
| Bahasa | Port | Adapter untuk test |
|---|---|---|
| Rust | trait | struct yang mengimplementasikan trait itu di memori |
| Python | typing.Protocol atau abstract base class | class kecil yang memegang list atau dict |
| Dart (Flutter) | abstract class | class yang mengimplementasikannya di memori |
| TypeScript (Node.js) | interface | object literal yang memenuhi interface itu |
| C | struct berisi function pointer | struct yang diisi fungsi-fungsi test |
Ada satu variasi yang sah. Di aplikasi Flutter, lapisan luar yang menghadap user berupa kumpulan layar, bukan request handler, jadi folder-nya bernama presentation/, bukan interface/. Posisinya di peta tetap sama.
Cara saya memeriksa module
Saya menyimpan daftar pertanyaan yang saya ajukan kepada module, bab demi bab, berdasarkan buku Martin. Daftar ini rutinitas tinjauan saya sendiri, bukan aturan dari bukunya, dan dari situlah saya memutuskan apakah struktur sebuah module sudah selesai:
| Bidang | Pertanyaan yang saya ajukan | Bab |
|---|---|---|
| Prinsip desain | Apakah tiap bagian mengikuti SRP, OCP, LSP, ISP, dan DIP? | 7–11 |
| Komponen | Apakah cohesion dan coupling-nya seimbang antara kemudahan diubah dan kemudahan dipakai ulang? | 13–14 |
| Batas | Apakah garisnya ditarik di tempat sistem bisa dipisahkan, dengan policy dan level tetap terpisah? | 16–19 |
| Aturan bisnis | Apakah aturannya berdiri sendiri, dan apakah strukturnya meneriakkan apa yang dikerjakan sistem, bukan framework yang dipakainya? | 20–21 |
| Intinya | Apakah tiap dependency menunjuk ke dalam? | 22 |
| Tepi | Apakah presenter-nya humble, apakah partial boundary-nya disengaja, dan apakah main component satu-satunya tempat merangkai semuanya? | 23–26 |
| Service dan test | Apakah service dan test menghormati batas yang sama dengan kodenya? | 27–28 |
| Detail | Apakah database, web, dan framework diperlakukan sebagai detail? | 30–32 |
Di mana saya tidak memakainya
Clean Architecture punya ongkos nyata: lebih banyak file, lebih banyak interface, dan satu langkah tambahan antara request dan kode yang melayaninya. Di tiap kasus di bawah, ongkos itu tak membelikan saya apa-apa, jadi saya memakai arsitektur lain. Bila arsitektur pengganti itu sudah punya halaman sendiri, kartunya ada di bawah.
A. Antarmuka web berbasis React atau Vue
Saya tidak memakainya untuk front end web dengan React atau Vue, dan di sana saya menganggapnya anti-pattern. Framework-framework itu sudah menentukan cara antarmuka disusun: komponen, state reaktif, routing. Aturan bisnis sistem saya tinggal di backend service di belakang antarmuka, bukan di browser. Membungkus aplikasi Vue atau React dengan lapisan domain dan use case berarti menggandakan apa yang sudah disediakan framework-nya, dan memaksa reaktivitasnya melewati abstraksi yang justru tak ia butuhkan.
Flutter pengecualiannya, dan di sana saya memakai Clean Architecture. Aplikasi Flutter saya membawa logika aplikasi sungguhan di perangkat — merekam audio dan video, memegang sesi, berbicara dengan beberapa service sekaligus — dan logika itu diuntungkan oleh port dan adapter yang sama seperti backend. Lapisan luarnya cukup dinamai presentation/.
Untuk antarmuka Vue saya memakai arsitektur ini:
B. Tool satu file
Tool yang muat dalam satu file tak mendapat lapisan sama sekali. Memecah script seratus baris ke empat folder akan membuat strukturnya lebih besar daripada programnya, dan mengubah kode yang bisa saya baca dari atas ke bawah menjadi kode yang harus dinavigasi. Untuk tool semacam ini, arsitektur paling jelas adalah satu file dengan urutan langkah yang jelas.
Bentuk yang saya berikan pada tool semacam itu:
C. Versi cepat pertama service Rust
Ketika service Rust harus cepat bekerja, atau ketika saya hanya bisa bekerja dengan AI agent lokal yang jendela konteksnya kecil, saya justru memulai dengan struktur datar: satu folder per jenis kode dan satu file per operasi. Struktur itu melepas port, jadi logika bisnisnya tahu database-nya, tapi cepat dibangun dan cukup kecil untuk tetap terlihat utuh. Ketika module tumbuh dan harus bertahan lama, saya merefactor-nya ke Clean Architecture, dan folder-folder datarnya langsung terpetakan ke lingkaran-lingkarannya.
Struktur datar tempat saya memulai:
D. Project terkunci framework dengan tim yang sudah ada
Ketika project dibangun di atas framework yang sudah menetapkan arsitekturnya sendiri, seperti Laravel, dan tim yang mengerjakannya menguasai framework itu, bukan Clean Architecture, saya mengikuti framework-nya. Laravel punya kesepakatannya sendiri untuk controller, model, dan service, dan tim bisa bekerja produktif di dalamnya. Memaksakan Clean Architecture di atasnya akan membelah codebase menjadi dua gaya, memperlambat semua orang, dan melawan framework di tiap langkah. Dalam keadaan itu, konsistensi dengan framework lebih berharga daripada manfaat lingkaran-lingkarannya.
E. Batas yang tak membayar dirinya sendiri
Bahkan di tempat saya memakainya, saya tak menarik semua batas yang digambarkan bukunya. Martin menyediakan satu bab untuk partial boundary, karena batas penuh di mana-mana jarang sepadan dengan harganya. Ketika dua use case hanya berbeda satu parameter, saya mempertahankan satu use case, bukan dua salinan yang nyaris kembar. Dependency Rule tetap utuh; jumlah file tak bertambah demi simetri. Ujian yang saya pakai adalah tujuan bukunya sendiri: bila sebuah batas membuat perubahan berikutnya lebih murah, batas itu bertahan; bila cuma menambah lapisan perantara, batas itu dibuang.
Rujukan
Robert C. Martin, Clean Architecture: A Craftsman's Guide to Software Structure and Design, Prentice Hall, 2017 — terutama bab tentang prinsip SOLID (7–11), batas (17–18), screaming architecture (21), Clean Architecture itu sendiri (22), partial boundary (24), main component (26), serta database, web, dan framework sebagai detail (30–32).
Istilah
- Policy
- Aturan yang menjadi alasan sistem itu ada, terlepas dari tool yang dipakai untuk menjalankannya.
- Detail
- Mekanisme yang menjalankan policy — database, web framework, client storage — dan bisa diganti tanpa mengubah policy-nya.
- Dependency Rule
- Dependency di source code hanya menunjuk ke dalam, dari detail ke policy.
- Use case
- Aturan khusus aplikasi: apa yang terjadi, dan dalam urutan apa, ketika seseorang meminta sistem mengerjakan sesuatu.
- Port
- Interface yang dideklarasikan lapisan dalam, menggambarkan kemampuan yang ia butuhkan dari luar.
- Adapter
- Implementasi port di lapisan luar, misalnya repository database atau versi in-memory untuk test.
- Main component
- Entry point program, dan satu-satunya tempat yang tahu adapter mana berdiri di balik tiap port.
- Partial boundary
- Batas yang ditarik di susunan source code tapi tak ditegakkan penuh, dipakai ketika batas lengkap lebih mahal daripada yang dihematnya.