Memahami Konsep Clean Architecture dalam Membangun Aplikasi yang Terstruktur

Clean architecture adalah cara menyusun kode aplikasi dalam lapisan-lapisan, dengan logika bisnis di pusat dan framework serta database di bagian luar. Bayangkan proyek yang awalnya kecil, lalu tumbuh bulan demi bulan. Aturan bisnis tersebar di controller, query database muncul di mana-mana, dan setiap perubahan kecil membuat tiga fitur lain ikut rusak. Untuk mengetes satu fungsi saja, Anda harus menyalakan database lebih dulu.

Kondisi seperti ini bukan tanda programmernya kurang pintar. Struktur kodenya memang tidak punya batas yang jelas antara “apa yang dilakukan aplikasi” dan “alat yang dipakai untuk melakukannya”. Ketika batas itu hilang, mengganti satu alat berarti mengubah hampir semuanya.

Artikel ini menjelaskan apa itu clean architecture, masalah yang muncul tanpanya, aturan dependensi yang menjadi intinya, empat lapisannya, dan lima alasan konsep ini membantu membangun aplikasi yang terstruktur. Anda juga akan menemukan contoh kode Node.js, cara mengujinya, serta kapan clean architecture justru tidak diperlukan.

Clean architecture dengan lapisan entities, use cases, interface adapters, dan frameworks untuk aplikasi terstruktur
Lapisan dalam berisi aturan bisnis, lapisan luar berisi alat seperti framework dan database.

Daftar Isi

Apa Itu Clean Architecture?

Clean architecture adalah gaya arsitektur perangkat lunak yang dipopulerkan Robert C. Martin, atau Uncle Bob. Dalam tulisannya tentang The Clean Architecture, ia menyatukan beberapa gagasan sebelumnya, seperti Hexagonal Architecture dari Alistair Cockburn dan Onion Architecture dari Jeffrey Palermo, ke dalam satu konsep yang bisa diterapkan.

Tujuan utamanya adalah memisahkan hal-hal yang penting dari hal-hal yang bisa berganti. Aturan bisnis, misalnya cara menghitung total pesanan, adalah hal penting yang jarang berubah. Framework web, database, dan tampilan adalah alat yang bisa berganti. Clean architecture menjaga agar alat tidak mengatur aturan bisnis.

Menurut Martin, arsitektur seperti ini menghasilkan sistem yang tidak bergantung pada framework, bisa diuji tanpa antarmuka atau database, serta tidak terikat pada UI maupun database tertentu. Dengan begitu, framework diperlakukan sebagai alat, bukan sebagai tempat Anda memaksakan seluruh sistem.

Masalah pada Aplikasi Tanpa Clean Architecture

Aplikasi yang tumbuh tanpa batas yang jelas biasanya menunjukkan gejala berikut:

  • Logika bisnis tersebar. Aturan yang sama muncul di controller, query, dan tampilan, sehingga mengubahnya berarti mengedit banyak tempat.
  • Kode terikat pada framework. Fungsi bisnis memakai objek request atau model ORM secara langsung, sehingga sulit dipakai di tempat lain.
  • Pengujian lambat dan rapuh. Setiap tes membutuhkan database atau server yang menyala.
  • Mengganti alat terasa menakutkan. Pindah database atau framework berarti menulis ulang sebagian besar kode.
  • Anggota baru kesulitan. Tidak jelas di mana sebuah aturan seharusnya ditaruh.

Pola seperti ini sering disebut “big ball of mud”, yaitu kode yang semuanya saling bergantung. Dasar perancangan data yang baik membantu, seperti dibahas di Mengapa Database Normalization Penting dalam Perancangan Sistem?, tetapi struktur kode juga perlu batas yang jelas.

Dependency Rule: Inti Clean Architecture

Aturan yang membuat seluruh konsep ini bekerja disebut dependency rule. Martin menyatakannya begini: dependensi kode sumber hanya boleh mengarah ke dalam. Tidak ada yang berada di lingkaran dalam yang boleh mengetahui apa pun tentang lingkaran luar.

Artinya, aturan bisnis tidak boleh mengimpor Express, Prisma, atau library database mana pun. Sebaliknya, lapisan luar boleh mengimpor lapisan dalam. Controller boleh memanggil use case, tetapi use case tidak boleh memanggil controller.

Lalu bagaimana jika use case perlu menyimpan data ke database? Di sinilah prinsip inversi dependensi bekerja. Use case mendefinisikan kontrak, misalnya “repository yang bisa menyimpan pesanan”. Lapisan luar kemudian menyediakan implementasinya dan memasukkannya ke use case. Panggilan tetap mengalir ke luar saat dijalankan, tetapi arah dependensi di kode tetap ke dalam.

4 Layer Clean Architecture

Diagram klasik clean architecture berbentuk lingkaran dengan empat lapisan. Lapisan paling dalam paling stabil, dan lapisan paling luar paling mudah berubah.

LapisanIsinyaContoh
1. EntitiesAturan bisnis inti yang berlaku di seluruh aplikasiPesanan, Pelanggan, aturan hitung total
2. Use CasesAturan bisnis khusus aplikasi dan alur kerjaBuatPesanan, BatalkanPesanan
3. Interface AdaptersPenerjemah data antara dunia luar dan use caseController, presenter, repository
4. Frameworks & DriversAlat dan detail teknisExpress, PostgreSQL, layanan pihak ketiga

1. Entities

Entities berisi aturan bisnis yang paling umum dan paling stabil. Contohnya, sebuah pesanan harus punya minimal satu item, atau total pesanan adalah jumlah harga dikali kuantitas. Lapisan ini tidak berubah hanya karena Anda mengganti framework, database, atau tampilan.

2. Use Cases

Use cases berisi aturan yang khusus untuk aplikasi Anda, yaitu langkah-langkah mencapai satu tujuan. Contohnya “buat pesanan”: validasi, buat entitas, simpan. Lapisan ini mengatur aliran data dari dan ke entities, tetapi tidak tahu apakah datanya datang dari HTTP, antrean, atau skrip.

3. Interface Adapters

Lapisan ini menerjemahkan data dari bentuk yang nyaman bagi use case ke bentuk yang nyaman bagi dunia luar, dan sebaliknya. Controller, presenter, dan implementasi repository berada di sini. Misalnya, controller mengubah body HTTP menjadi input use case.

4. Frameworks and Drivers

Ini lapisan paling luar, tempat semua alat berada: framework web, driver database, dan layanan eksternal. Lapisan ini hanya berisi sedikit kode perekat. Semua yang penting sudah ada di lapisan dalam.

Martin sendiri menyebut bahwa empat lapisan bukan aturan mutlak. Anda boleh punya lebih banyak, asalkan dependency rule tetap dipatuhi.

5 Alasan Clean Architecture Penting untuk Aplikasi Terstruktur

1. Clean Architecture Membuat Logika Bisnis Mudah Ditemukan

Setiap aturan punya rumah yang jelas. Jika aturan total pesanan berubah, Anda tahu harus membuka entitas pesanan. Anggota tim baru juga lebih cepat memahami kode karena strukturnya konsisten.

2. Aplikasi Lebih Mudah Diuji

Karena logika bisnis tidak bergantung pada framework atau database, Anda bisa mengujinya dengan cepat tanpa menyalakan apa pun. Cukup berikan repository palsu ke use case, lalu periksa hasilnya.

3. Alat Bisa Diganti dengan Risiko Kecil

Ingin pindah dari database satu ke database lain, atau dari Express ke framework lain? Anda cukup menulis adapter baru, sementara entities dan use cases tetap utuh. Perubahan tersebar di satu lapisan, bukan seluruh kode.

4. Perubahan Terisolasi

Perubahan di tampilan tidak menyentuh aturan bisnis, dan perubahan aturan bisnis tidak memaksa Anda mengubah database. Isolasi ini mengurangi efek berantai yang membuat perubahan kecil terasa berisiko.

5. Kolaborasi Tim Lebih Rapi

Beberapa developer bisa mengerjakan lapisan berbeda secara paralel. Satu orang mengerjakan use case, lain mengerjakan adapter database. Dengan kontrak yang jelas antarlapisan, konflik kode pun berkurang. Perubahan struktur database juga lebih terkendali jika dikelola dengan migrasi, seperti di Mengenal Database Migration dan Pentingnya dalam Kolaborasi Developer.

Dependency rule pada clean architecture dengan panah dependensi mengarah ke lapisan dalam
Semua panah dependensi mengarah ke dalam, menuju aturan bisnis.

Contoh Clean Architecture dengan Node.js

Mari terapkan konsepnya pada fitur sederhana: membuat pesanan lewat API. Contoh ini memakai JavaScript dan Express, dengan struktur folder berikut.

src/
  domain/
    pesanan.js                        # entities
  usecases/
    buat-pesanan.js                   # use cases
  adapters/
    http/
      pesanan-controller.js           # interface adapters
    repositories/
      pesanan-repository-memori.js    # interface adapters
  main.js                             # frameworks & drivers, perakit aplikasi

Pertama, entitas berisi aturan bisnis yang murni. Tidak ada impor dari Express atau database.

// src/domain/pesanan.js
export class Pesanan {
  constructor({ id, idPelanggan, items }) {
    if (!items || items.length === 0) {
      throw new Error('Pesanan harus memiliki minimal satu item');
    }
    this.id = id;
    this.idPelanggan = idPelanggan;
    this.items = items;
    this.status = 'menunggu_pembayaran';
  }

  hitungTotal() {
    return this.items.reduce((total, item) => total + item.harga * item.jumlah, 0);
  }
}

Kedua, use case mengatur alurnya. Perhatikan bahwa use case menerima repository lewat constructor. Ia tidak tahu apakah repository itu memakai memori, PostgreSQL, atau yang lain.

// src/usecases/buat-pesanan.js
import { Pesanan } from '../domain/pesanan.js';

export class BuatPesanan {
  constructor(pesananRepository) {
    this.pesananRepository = pesananRepository;
  }

  async jalankan({ idPelanggan, items }) {
    const pesanan = new Pesanan({ id: null, idPelanggan, items });
    const tersimpan = await this.pesananRepository.simpan(pesanan);

    return {
      id: tersimpan.id,
      total: tersimpan.hitungTotal(),
      status: tersimpan.status
    };
  }
}

Ketiga, adapter. Repository menyimpan data, dan controller menerjemahkan HTTP ke use case. Di sinilah detail teknis boleh muncul.

// src/adapters/repositories/pesanan-repository-memori.js
export class PesananRepositoryMemori {
  constructor() {
    this.data = [];
  }

  async simpan(pesanan) {
    pesanan.id = this.data.length + 1;
    this.data.push(pesanan);
    return pesanan;
  }
}

// src/adapters/http/pesanan-controller.js
export function buatPesananController(buatPesanan) {
  return async (req, res) => {
    try {
      const hasil = await buatPesanan.jalankan(req.body);
      res.status(201).json(hasil);
    } catch (err) {
      res.status(400).json({ error: err.message });
    }
  };
}

Keempat, berkas utama merakit semuanya. Berkas ini satu-satunya tempat yang mengenal semua lapisan. Konfigurasi seperti alamat database sebaiknya dibaca dari variabel lingkungan, seperti dibahas di Environment Variable: Cara Pakai .env di Node.js.

// src/main.js
import express from 'express';
import { BuatPesanan } from './usecases/buat-pesanan.js';
import { PesananRepositoryMemori } from './adapters/repositories/pesanan-repository-memori.js';
import { buatPesananController } from './adapters/http/pesanan-controller.js';

const app = express();
app.use(express.json());

const pesananRepository = new PesananRepositoryMemori();
const buatPesanan = new BuatPesanan(pesananRepository);

app.post('/pesanan', buatPesananController(buatPesanan));

app.listen(3000);

Sekarang bayangkan Anda ingin berpindah dari memori ke PostgreSQL. Anda cukup membuat PesananRepositoryPostgres yang punya metode simpan yang sama, lalu mengganti satu baris di main.js. Entitas, use case, dan controller tidak berubah sama sekali. Jika Anda ingin mengganti pola API, pertimbangannya mirip dengan pembahasan di REST API vs GraphQL: Mana yang Lebih Cocok untuk Proyek Web Modern?, karena hanya adapter yang berubah.

Contoh ini sengaja disederhanakan. Controller menangkap semua error sebagai 400, padahal kode produksi sebaiknya membedakan kesalahan input dari kesalahan sistem. Anda juga perlu menyiapkan validasi data masuk dan penanganan kesalahan yang lebih rapi.

Struktur folder clean architecture di Node.js dengan domain, usecases, dan adapters
Struktur folder mencerminkan lapisan: domain, usecases, dan adapters.

Cara Menguji Aplikasi dengan Clean Architecture

Keuntungan terbesar terasa saat pengujian. Karena use case menerima repository dari luar, Anda bisa mengujinya tanpa database. Contoh berikut memakai test runner bawaan Node.js dan repository memori sebagai pengganti.

// tests/buat-pesanan.test.js
import test from 'node:test';
import assert from 'node:assert/strict';
import { BuatPesanan } from '../src/usecases/buat-pesanan.js';
import { PesananRepositoryMemori } from '../src/adapters/repositories/pesanan-repository-memori.js';

test('BuatPesanan menghitung total dengan benar', async () => {
  const buatPesanan = new BuatPesanan(new PesananRepositoryMemori());

  const hasil = await buatPesanan.jalankan({
    idPelanggan: 1,
    items: [{ harga: 50000, jumlah: 2 }]
  });

  assert.equal(hasil.total, 100000);
});

test('BuatPesanan menolak pesanan tanpa item', async () => {
  const buatPesanan = new BuatPesanan(new PesananRepositoryMemori());

  await assert.rejects(
    () => buatPesanan.jalankan({ idPelanggan: 1, items: [] }),
    /minimal satu item/
  );
});

Strategi pengujian yang umum terbagi tiga. Uji entitas dan use case secara cepat tanpa dependensi luar. Uji adapter, seperti repository database, dengan database uji sungguhan untuk memastikan query-nya benar. Terakhir, uji beberapa alur utama secara menyeluruh lewat API.

Kapan Clean Architecture Tidak Diperlukan?

Clean architecture bukan obat untuk semua proyek. Pemisahan lapisan menambah berkas, kontrak, dan penerjemahan data. Untuk proyek tertentu, biaya itu belum sebanding dengan manfaatnya:

  • Prototipe dan proyek singkat. Jika kode akan dibuang bulan depan, struktur yang ringan lebih masuk akal.
  • Aplikasi CRUD sederhana. Jika hampir tidak ada aturan bisnis, lapisan use case hanya meneruskan data.
  • Tim sangat kecil dengan waktu terbatas. Disiplin arsitektur perlu dijaga, dan itu membutuhkan waktu.
  • Skrip atau alat sekali pakai. Tidak perlu arsitektur berlapis.

Pendekatan yang sehat adalah memulai sederhana, lalu menambah batas ketika kerumitan mulai terasa. Anda tidak harus menerapkan keempat lapisan sejak hari pertama. Mulailah dengan memisahkan aturan bisnis dari framework, lalu kembangkan sesuai kebutuhan. Sistem kontrol versi membantu eksperimen seperti ini, seperti di Git Adalah: Pengertian, Cara Kerja, dan 10 Perintah Dasar.

Kesalahan Umum dalam Menerapkan Clean Architecture

  • Hanya meniru nama folder. Folder bernama entities dan usecases tidak berarti apa-apa jika isinya masih mengimpor framework.
  • Membiarkan model ORM bocor ke use case. Use case sebaiknya bekerja dengan entitas, bukan objek bawaan database.
  • Memberi use case akses ke objek request atau response. Itu melanggar dependency rule karena use case jadi tahu soal HTTP.
  • Menaruh logika bisnis di controller. Controller hanya menerjemahkan, bukan memutuskan aturan.
  • Membuat terlalu banyak lapisan dan pemetaan. Setiap lapisan tambahan punya biaya, jadi tambahkan hanya jika ada manfaat nyata.
  • Membuat entitas yang hanya berisi data. Aturan yang menempel pada data sebaiknya berada di entitas itu.
  • Menerapkan arsitektur tanpa alasan. Pilih berdasarkan masalah yang ingin diatasi, bukan karena tren.

Pertanyaan yang Sering Diajukan tentang Clean Architecture

Apakah clean architecture sama dengan MVC?

Tidak sama. MVC memisahkan tampilan, kontrol, dan model, dan semuanya berada di lapisan interface adapters dalam clean architecture. Clean architecture menambahkan lapisan use cases dan entities yang tidak bergantung pada framework tersebut.

Apakah harus memakai empat lapisan?

Tidak. Empat lapisan hanyalah gambaran umum. Yang wajib dipatuhi adalah dependency rule, yaitu dependensi hanya mengarah ke dalam. Jumlah lapisan boleh lebih banyak atau lebih sedikit sesuai kebutuhan.

Apakah clean architecture cocok untuk proyek kecil?

Bisa, tetapi dengan versi ringan. Cukup pisahkan aturan bisnis dari framework dan database. Untuk proyek yang sangat sederhana, struktur penuh mungkin berlebihan.

Apakah hanya untuk backend?

Tidak. Prinsip memisahkan aturan inti dari detail teknis bisa diterapkan pada aplikasi mobile, desktop, maupun frontend. Contoh di artikel ini memakai backend Node.js agar mudah diikuti.

Daftar Periksa Penerapan Clean Architecture

  • Apakah aturan bisnis bebas dari impor framework dan database?
  • Apakah dependensi kode hanya mengarah ke dalam?
  • Apakah use case menerima repository dari luar, bukan membuatnya sendiri?
  • Apakah controller hanya menerjemahkan data, tanpa aturan bisnis?
  • Apakah use case bisa diuji tanpa database atau server?
  • Apakah mengganti database cukup dengan menulis adapter baru?
  • Apakah lapisan yang Anda buat benar-benar dibutuhkan proyek ini?
  • Apakah anggota tim tahu di mana setiap jenis kode harus diletakkan?

Kesimpulan: Clean Architecture Membantu Aplikasi Tetap Terstruktur

Clean architecture memisahkan aturan bisnis dari alat teknis lewat lapisan dan satu aturan sederhana: dependensi hanya mengarah ke dalam. Dengan begitu, logika bisnis mudah ditemukan, mudah diuji, dan tidak ikut berubah saat framework atau database diganti. Hasilnya, aplikasi lebih terstruktur dan lebih mudah dirawat oleh tim.

Meski begitu, konsep ini bukan keharusan untuk setiap proyek. Mulailah dari batas paling penting, yaitu memisahkan aturan bisnis dari framework, lalu tambah lapisan ketika kerumitan menuntutnya. Dengan pendekatan bertahap, Anda mendapat manfaat arsitektur tanpa terjebak pada kerumitan yang tidak perlu.

Artikel Lainnya di NgodingDaily

Lanjutkan belajar dengan topik lain yang saling melengkapi:

Leave a Reply

Your email address will not be published. Required fields are marked *