Seseorang di tim Anda baru saja mengimpor pg ke src/domain/invoice.ts. PR-nya berhasil dikompilasi. Tes-tesnya lulus. Code review-nya tiga ratus baris panjangnya, dan tidak ada yang menyadarinya.

Tiga bulan kemudian, Anda ingin mengekstrak logika domain ke dalam package bersama. Anda tidak bisa. Logika itu bergantung pada tipe Postgres, logika connection pooling, dan wrapper driver kustom yang hanya ada di monolit. Diagram papan tulis dengan lingkaran konsentris kini menjadi lelucon.

Ini adalah pola pembusukan standar dari clean architecture tanpa penegakan otomatis. Arsitek menggambar panah yang menunjuk ke dalam. Developer menulis impor yang menunjuk ke luar. Compiler tidak peduli dengan lapisan-lapisan Anda.

Apa yang sebenarnya dimaksud dengan “domain tidak mengimpor infrastruktur”

Dalam arsitektur berlapis, dependency mengalir ke dalam. Domain layer, yang berada di pusat, mendefinisikan entitas, business rules, dan use case. Domain layer tidak tahu apa-apa tentang HTTP, SQL, queue, atau file system.

Infrastruktur berada di cincin luar. Infrastruktur mengimplementasikan interface yang didefinisikan oleh domain. Repository interface berada di domain. Implementasi Postgres berada di infrastruktur. Domain memanggil save(invoice). Domain tidak pernah memanggil pool.query().

Ketika kode domain mengimpor infrastruktur secara langsung, dua hal terjadi. Pertama, domain menjadi tidak bisa diuji tanpa database yang berjalan. Kedua, Anda tidak pernah bisa mengganti Postgres dengan DynamoDB tanpa menulis ulang business logic. Seluruh tujuan dari penlapisan, testability dan swapability, lenyap.

Mengapa code review tidak cukup

Setiap tim memiliki engineer senior yang menangkap impor buruk saat review. Engineer itu juga sedang libur, sakit, atau sedang mereview PR lima puluh file pada pukul 5 sore di hari Jumat.

Manusia adalah pattern matcher dengan RAM terbatas. Aturan dependency otomatis adalah validator deterministik dengan kesabaran tak terbatas. Tempat yang tepat untuk menegakkan batasan arsitektur adalah tempat yang sama dengan tempat Anda menegakkan error sintaks: build pipeline. Jika berhasil dikompilasi tapi melanggar dependency graph, build harus gagal.

Bagaimana dependency-cruiser menegakkan batasan layer

Untuk codebase TypeScript dan JavaScript, dependency-cruiser mengubah diagram arsitektur Anda menjadi sekumpulan aturan yang bisa diuji. Ia menguraikan import graph Anda dan membuat build gagal ketika ada edge terlarang yang muncul.

Instal:

npm install --save-dev dependency-cruiser
npx depcruise --init

Script init menghasilkan konfigurasi .dependency-cruiser.js. Anda menambahkan aturan yang melarang domain menyentuh infrastruktur:

// .dependency-cruiser.js
module.exports = {
  forbidden: [
    {
      name: "domain-cannot-import-infrastructure",
      comment:
        "Domain code must not depend on infrastructure. Move the import to an adapter or repository.",
      severity: "error",
      from: {
        path: "^src/domain",
      },
      to: {
        path: "^src/infrastructure",
      },
    },
    {
      name: "domain-cannot-import-node-modules",
      comment:
        "Domain code must not depend on external I/O libraries.",
      severity: "error",
      from: {
        path: "^src/domain",
      },
      to: {
        dependencyTypes: ["npm"],
        // Allow only pure logic libraries like date-fns or lodash
        pathNot: "^(date-fns|lodash|ramda)",
      },
    },
  ],
  options: {
    doNotFollow: {
      path: "node_modules",
    },
    tsPreCompilationDeps: true,
    tsConfig: {
      fileName: "tsconfig.json",
    },
  },
};

Aturan ini menyatakan: file apa pun di bawah src/domain yang mengimpor apa pun di bawah src/infrastructure membuat build gagal. Aturan kedua menangkap impor pg atau axios yang masuk melalui node_modules.

Sambungkan ke CI Anda:

# .github/workflows/ci.yml
jobs:
  architecture:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - run: npx depcruise src

Sekarang impor pg di src/domain/invoice.ts gagal bahkan sebelum manusia membuka PR.

Seperti apa kegagalannya

Ketika seorang developer melanggar aturan, mereka mendapatkan output seperti ini:

error domain-cannot-import-infrastructure: src/domain/invoice.ts → src/infrastructure/database/pool.ts
  Domain code must not depend on infrastructure. Move the import to an adapter or repository.

error domain-cannot-import-node-modules: src/domain/invoice.ts → pg
  Domain code must not depend on external I/O libraries.

✖ 2 dependency violations (2 errors, 0 warnings). Showing only the first 2.

Pesan error memberi tahu mereka apa yang harus dilakukan. Pindahkan panggilan database ke repository di layer infrastruktur. Berikan repository ke use case sebagai interface. Kode domain tetap murni.

Alternatif untuk ekosistem lain

Tidak semua orang menggunakan TypeScript. Prinsipnya sama di mana-mana. Hanya tooling yang berubah.

Java: ArchUnit adalah standar emas. Anda menulis test, bukan file konfigurasi:

@ArchTest
static final ArchRule domain_should_not_access_infrastructure =
    noClasses()
        .that()
        .resideInAPackage("..domain..")
        .should()
        .dependOnClassesThat()
        .resideInAPackage("..infrastructure..");

Ini berjalan sebagai test JUnit, sehingga terintegrasi dengan Maven dan Gradle tanpa plumbing CI baru apa pun.

C#: NetArchTest menyediakan gaya API yang sama untuk .NET. Tulis unit test, jalankan di CI.

Go: Tidak ada setara yang matang. Kebanyakan tim menegakkan ini dengan shell script sederhana:

#!/bin/bash
# scripts/check-domain-imports.sh

if grep -r "infrastructure/" internal/domain/ ; then
  echo "Domain code imports infrastructure. Fix the dependency direction."
  exit 1
fi

Ini kasar, tapi berhasil. Banyak tim Go juga menggunakan analyzer go vet kustom atau package go/ast untuk membangun pemeriksaan yang tepat.

Build system: Bazel, Gradle, dan Nx semuanya mendukung module dependency graph secara native. Jika Anda mendefinisikan domain dan infrastruktur sebagai module terpisah dengan deklarasi dependency eksplisit, build tool menegakkan batasan secara gratis. Masalahnya adalah Anda harus mengadopsi build system terlebih dahulu.

Trade-off: false positive, migration pain, dan team friction

Aturan otomatis tidak gratis. Anda harus mengetahui biayanya sebelum menghidupkannya.

False positive terjadi ketika Anda memiliki utility bersama yang sah yang berada di antara layer. Sebuah slugifier string atau custom error base class mungkin berada di src/shared. Jika domain dan infrastruktur sama-sama mengimpornya, aturannya baik-baik saja. Jika Anda meletakkannya di src/infrastructure/utils dan domain mengimpornya, dependency-cruiser akan mengeluh. Perbaikannya biasanya memindahkan kode bersama, yang seringkali adalah langkah yang tepat.

Migration pain adalah nyata. Jika codebase Anda sudah melanggar aturan di tiga puluh tempat, Anda tidak bisa membalik sakelar tanpa refactoring heroik. Pendekatan pragmatis adalah memulai dengan warning, memperbaiki pelanggaran yang ada selama satu sprint, lalu menaikkannya ke error. Atau, gunakan exception pathNot untuk file legacy yang diketahui dan larang pelanggaran baru sejak hari pertama.

Team friction adalah biaya tersembunyi. Developer yang belum pernah bekerja dengan penegakan layer yang ketat akan menolak. Mereka akan berargumen bahwa satu impor kecil tidak berbahaya, bahwa aturan itu birokratis, bahwa aturan itu memperlambat mereka. Ini adalah sinyal budaya, bukan teknis. Developer yang sama juga akan menjadi orang yang mengimpor express ke domain model pada pukul 11 malam untuk mengirim fitur. Aturan ini ada karena manusia di bawah tekanan mengambil jalan pintas.

Cara mengimplementasikan ini di codebase yang sudah ada

Anda tidak perlu rewrite besar. Anda membutuhkan batasan dan cara untuk mengukurnya.

  1. Gambar layer-layer-nya. Tentukan direktori mana yang merupakan domain, application, dan infrastruktur. Tulis di ARCHITECTURE.md.

  2. Tambahkan dependency-cruiser (atau setara di ekosistem Anda) dengan satu aturan: domain tidak boleh mengimpor infrastruktur.

  3. Jalankan. Hitung pelanggarannya. Jika jumlahnya kecil, perbaiki sebelum menggabungkan konfigurasi. Jika besar, tambahkan path legacy ke exception pathNot.

  4. Buat agar gagal di CI. Bukan opsional. Bukan warning. Sebuah error yang memblokir merge.

  5. Setiap kali developer menemui error, bantu mereka memindahkan impor. Jangan hanya memberi tahu bahwa build-nya merah. Jelaskan di mana kode seharusnya berada dan mengapa.

Dalam dua minggu, tim akan berhenti mencoba mengimpor pg ke file domain. Diagram arsitektur di papan tulis akhirnya akan cocok dengan kode di repository.

FAQ

Apakah application layer boleh mengimpor infrastruktur?

Biasanya, ya. Application layer mengorkestrasikan use case. Application layer boleh tahu tentang repository dan external service, tapi harus bergantung pada interface, bukan concrete implementation. Jika Anda ingin penegakan yang ketat, tambahkan aturan kedua: application tidak boleh mengimpor implementasi infrastruktur, hanya interface-nya.

Bagaimana dengan domain event?

Domain event adalah celah umum. Domain event adalah data murni, sehingga termasuk dalam domain layer. Event bus infrastruktur yang mempublikasikannya termasuk dalam infrastruktur. Application layer mendaftarkan domain event handler ke bus. Domain itu sendiri tidak pernah tahu bus ada.

Bisakah saya menggunakan ini dengan Nx atau Turborepo?

Ya. Nx memiliki aturan module boundary bawaan yang bekerja di tingkat proyek. Jika domain dan infrastruktur Anda adalah library Nx yang terpisah, Anda bisa menegakkan aturan tanpa tool tambahan apa pun. Hal yang sama berlaku untuk Bazel dan Gradle.

Bagaimana jika saya membutuhkan utility dari infrastruktur?

Anda tidak perlu. Pindahkan utility ke package common atau kernel bersama yang tidak memiliki dependency infrastruktur. Jika utility itu benar-benar membutuhkan infrastruktur, itu bukan utility. Itu adalah infrastruktur.

Intinya

Clean architecture tanpa penegakan otomatis adalah gentleman’s agreement. Gentleman’s agreement tidak bertahan menghadapi deadline produksi.

Dependency-cruiser, ArchUnit, atau bahkan shell script dengan grep mengubah arsitektur Anda dari saran menjadi jaminan. Domain tetap murni. Infrastruktur tetap bisa diganti. Dan lain kali seseorang mencoba mengimpor pg ke business rule, build gagal sebelum PR mencapai reviewer manusia.

Mulai dengan satu aturan. Buat menjadi merah di CI. Perbaiki pelanggaran pertama. Yang lainnya akan mengikuti.