Skip to content
Architecture Programming

Clean Architecture

By Romi Fadli, S.Kom. 12 min read Software Design
On this page
Four concentric circles with arrows coming in from a database, a browser window, and a message queue, all pointing toward the centre.

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

AsalRobert C. Martin, Clean Architecture (2017)
Saya pakai untukBackend service dan aplikasi yang aturannya semestinya bertahan lebih lama daripada tool-nya, termasuk aplikasi Flutter
Tak saya pakai untukAntarmuka web berbasis React atau Vue, tool satu file, dan project yang terkunci framework seperti Laravel
BahasaRust, Python, Dart (Flutter), Node.js, C
Posisi Clean Architecture di antara arsitektur yang saya pakai.

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:

bash
src/
├── domain/          # entities, value objects, error domain — tak meng-import apa pun dari luar
├── application/     # use_cases/, ports/, dto/, validation/
├── infrastructure/  # adapter: persistence/, config/, telemetry/, bootstrap/
├── interface/       # pintu masuk dari luar: http/, presenters/
├── shared/          # helper kecil tanpa makna bisnis
└── main             # main component: memilih adapter untuk tiap port
LingkaranFolderIsinya
Entitiesdomain/entities, value objects, error domain
Use casesapplication/use case, port yang dibutuhkannya, tipe input, validasi
Interface adaptersinterface/request handler, presenter, penerjemahan error ke response
Frameworks and driversinfrastructure/adapter untuk database, storage, telemetry, konfigurasi
Cara lingkaran-lingkaran Martin dipetakan ke folder saya.

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:

rust
// application/ports: yang dibutuhkan use case
pub trait RecordStore {
    fn save(&self, record: &Record) -> Result<(), StoreError>;
}

// infrastructure/persistence: salah satu cara menyediakannya
pub struct PostgresRecordStore { /* connection pool */ }
impl RecordStore for PostgresRecordStore { /* … */ }

Bentuk yang sama berlaku di semua bahasa yang saya pakai; yang berubah cuma sebutan untuk "kemampuan tanpa implementasi":

BahasaPortAdapter untuk test
Rusttraitstruct yang mengimplementasikan trait itu di memori
Pythontyping.Protocol atau abstract base classclass kecil yang memegang list atau dict
Dart (Flutter)abstract classclass yang mengimplementasikannya di memori
TypeScript (Node.js)interfaceobject literal yang memenuhi interface itu
Cstruct berisi function pointerstruct yang diisi fungsi-fungsi test
Cara menulis port dan adapter test-nya di tiap bahasa.

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:

BidangPertanyaan yang saya ajukanBab
Prinsip desainApakah tiap bagian mengikuti SRP, OCP, LSP, ISP, dan DIP?7–11
KomponenApakah cohesion dan coupling-nya seimbang antara kemudahan diubah dan kemudahan dipakai ulang?13–14
BatasApakah garisnya ditarik di tempat sistem bisa dipisahkan, dengan policy dan level tetap terpisah?16–19
Aturan bisnisApakah aturannya berdiri sendiri, dan apakah strukturnya meneriakkan apa yang dikerjakan sistem, bukan framework yang dipakainya?20–21
IntinyaApakah tiap dependency menunjuk ke dalam?22
TepiApakah presenter-nya humble, apakah partial boundary-nya disengaja, dan apakah main component satu-satunya tempat merangkai semuanya?23–26
Service dan testApakah service dan test menghormati batas yang sama dengan kodenya?27–28
DetailApakah database, web, dan framework diperlakukan sebagai detail?30–32
Pertanyaan yang saya pakai untuk memeriksa module, dikelompokkan menurut bab asalnya.

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.

About the author

Photo of Romi Fadli

Romi Fadli, S.Kom.

Solution Architect · Backend Engineer · Embedded Systems

In my day job I work as a solution architect. I design the system, lead the team that builds it — quality assurance, backend developers, a UI developer, DevOps and DevSecOps — and write the most critical modules myself, such as a certificate authority, a long-term PDF signer, and the core API of a personnel system.

I work across the whole stack that a running system actually needs. On the hardware side that means PCB design, microcontrollers, and embedded firmware — including RISC-V boards running FreeRTOS and Embassy, speaking MQTT over mutually authenticated TLS on a microcontroller with 64 KB of SRAM.

Most of my day-to-day work is software: Rust backends and API modules built on Clean Architecture, an OAuth2/OpenID Connect authorization server written from scratch, PAdES document signing, and certificate authorities — root CAs, timestamping authorities, and OCSP responders. I also build the frontends that sit on top of them.

Running all of that means infrastructure: Docker Compose stacks, networking, Mikrotik and RADIUS, observability with OpenTelemetry, and a steady habit of writing command-line tools and utilities for Linux when the existing ones do not fit.

Share

Enjoyed this?

Get an email when a new article is published.

Referenced by

Three identical empty application nodes stacked as copies, connected to a database, a cache, object storage, a pool of queue workers, and a telemetry collector that hold all the state

Cara saya menjalankan Laravel sebagai inti platform yang dibangun tim: node tanpa state, pekerjaan lambat diserahkan ke job dan service khusus, file dijauhkan dari server, satu token internal untuk setiap login, dan setiap request, job, serta panggilan bisa diamati.

13 min read Read more →
One endpoint procedure as a column of ordered steps, a request entering at the top and the answer leaving at the bottom, each step calling its own concern module beside it

Tiap endpoint adalah satu prosedur yang membawa request dari awal sampai selesai, dengan urusannya di module di sampingnya — Transaction Script Fowler dalam bentuk yang saya berikan pada service Python kecil bertugas tunggal, dibundel jadi satu binary, sampai aturannya tumbuh.

8 min read Read more →
A four-level tree of Indonesian regions, with one chosen path coloured level by level, feeding a form of four cascading dropdowns in the same colours

Service Rust yang menyajikan wilayah administratif Indonesia — 38 provinsi sampai 83.768 desa, berkunci kode resmi Kementerian Dalam Negeri — sebagai empat lookup bertingkat dengan pencarian, supaya setiap form alamat di setiap project mendapat jawaban yang sama.

10 min read Read more →
A features folder holding three feature folders — orders, users, reports — with a blue line flowing one way into stores, services and an HTTP client, and a purple line to a design system pinned at version 1.0.0

Feature-Based Frontend Architecture adalah salah satu arsitektur yang saya pakai: bentuk yang saya berikan pada antarmuka web Vue — satu folder per fitur, kode bersama di beberapa folder global per jenis, data mengalir satu arah lewat store dan service ke satu HTTP client, dan design system saya yang dikunci per versi.

7 min read Read more →
Four flat layers stacked on the left — routes, controllers, services, repositories — joined by short lines, with an orange line labelled later: refactor leading to the concentric circles of Clean Architecture on the right

Satu folder per jenis kode di satu tingkat, jalur pendek dari route ke repository, satu file per operasi — bentuk yang saya berikan pada service Rust yang harus cepat jadi atau muat di konteks AI agent lokal, sebelum tumbuh jadi Clean Architecture.

7 min read Read more →
A single file card holding six sections in a fixed order — Config, Helpers, Reporter, Steps, Summary, Entrypoint — with arguments, a config file and a prompt flowing in from the left and exit codes 0, 1, 2 and 130 flowing out to the right

Bentuk yang saya berikan pada tool kecil: satu file, satu tugas, seksi berurutan tetap, kontrak exit code di bagian atas, dan tiga pintu masuk input — ditambah varian Bash untuk generator, dan kapan tool sudah terlalu besar untuknya.

8 min read Read more →
A browser window connected directly to a storage bucket by a thick green band, while an orange box with a key above them hands the browser a dashed link.

Service Rust kecil di antara aplikasi dan storage kompatibel S3: ia membagikan presigned link supaya browser berbicara langsung dengan storage, memberi tiap file siklus hidup, menyalin antar-bucket di sisi server, dan menjauhkan kredensial storage dari setiap aplikasi.

12 min read Read more →

Related Articles

A four-level tree of Indonesian regions, with one chosen path coloured level by level, feeding a form of four cascading dropdowns in the same colours

Service Rust yang menyajikan wilayah administratif Indonesia — 38 provinsi sampai 83.768 desa, berkunci kode resmi Kementerian Dalam Negeri — sebagai empat lookup bertingkat dengan pencarian, supaya setiap form alamat di setiap project mendapat jawaban yang sama.

10 min read Read more →
A browser window connected directly to a storage bucket by a thick green band, while an orange box with a key above them hands the browser a dashed link.

Service Rust kecil di antara aplikasi dan storage kompatibel S3: ia membagikan presigned link supaya browser berbicara langsung dengan storage, memberi tiap file siklus hidup, menyalin antar-bucket di sisi server, dan menjauhkan kredensial storage dari setiap aplikasi.

12 min read Read more →