Monolit Ruby on Rails kerap mengalami degradasi arsitektur seiring bertambahnya ukuran tim dan basis kode. Skema autoloading Zeitwerk memungkinkan setiap konstanta diakses secara global, yang memicu munculnya kopling tak terkendali antar domain. Memecah aplikasi menjadi arsitektur microservices sering kali prematur dan menambah beban operasional. Packwerk menawarkan jalan tengah: modular monolith dengan validasi batas domain berbasis analisis statis tanpa runtime overhead.

Akar Masalah: Spaghetti Dependency pada Rails

Secara default, direktori app/models, app/services, dan komponen Rails lainnya berbagi namespace terbuka. Service domain pembayaran dapat langsung memanggil model internal domain logistik tanpa kontrak yang jelas:

# app/services/billing/checkout_service.rb
class Billing::CheckoutService
  def call(order_id)
    order = Order.find(order_id)
    # Pelanggaran boundary: Billing langsung memanipulasi state internal shipping
    Shipping::InternalFulfillmentJob.perform_later(order.shipping_manifest_id)
  end
end

Kopling implisit seperti ini menyulitkan isolasi pengujian, memperlambat proses code review, dan menimbulkan efek samping tersembunyi ketika skema internal suatu modul diubah.

Konfigurasi Package Boundaries dengan Packwerk

Packwerk membagi basis kode menjadi paket-paket terisolasi menggunakan konvensi direktori dan file metadata. Tambahkan gem Packwerk ke dalam Gemfile:

group :development, :test do
  gem 'packwerk', require: false
end

Inisialisasi konfigurasi dasar melalui terminal:

bundle exec packwerk init

Perintah tersebut menghasilkan file root packwerk.yml. Untuk membuat paket modular, letakkan kode pada direktori terpisah (misalnya packages/billing) dan definisikan file package.yml di dalamnya:

# packages/billing/package.yml
enforce_dependencies: true
enforce_privacy: true
dependencies:
  - packages/orders
  - packages/notifications
metadata:
  domain: finance
  owner: team-billing

Dua aturan utama yang ditegakkan oleh Packwerk:

  • Enforce Dependencies: Paket ini hanya boleh mengakses konstanta dari paket yang terdaftar pada deklarasi dependencies.
  • Enforce Privacy: Hanya konstanta di dalam direktori packages/billing/app/public/ yang dapat diakses oleh paket lain. Konstanta di luar folder publik dianggap privat.

Public API Pattern untuk Domain Terisolasi

Buat interface eksplisit untuk mengekspos fungsi modul ke domain lain. Tempatkan file API pada folder publik:

# packages/billing/app/public/billing/charge_client.rb
module Billing
  class ChargeClient
    def self.process_payment(order_id:, amount_cents:)
      # Delegasikan eksekusi ke service privat
      Billing::InternalProcessor.new(order_id, amount_cents).execute
    end
  end
end

Domain eksternal kini wajib memanggil Billing::ChargeClient.process_payment dan dilarang menyentuh Billing::InternalProcessor secara langsung.

Automasi Boundary Enforcement di GitHub Actions

Untuk mencegah regresi dependensi baru masuk ke main branch, validasi Packwerk harus dijalankan pada pipeline CI.

# .github/workflows/packwerk.yml
name: Modular Boundary Check

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

jobs:
  packwerk:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Ruby
        uses: ruby/setup-ruby@v1
        with:
          bundler-cache: true

      - name: Run Packwerk Validation
        run: |
          bundle exec packwerk check

Perintah packwerk check melakukan pemindaian sintaks dan mencocokkan referensi konstanta terhadap aturan di setiap package.yml. Proses ini memakan waktu beberapa detik karena murni berjalan secara statis via parser AST tanpa perlu memuat seluruh lingkungan Rails.

Strategi Refactoring Bertahap via Deprecations

Mengubah monolit lama secara instan ke struktur modular yang bersih tidak realistis. Packwerk menyediakan mekanisme baseline pelanggaran yang sudah ada menggunakan file deprecated_references.yml:

bundle exec packwerk update-deprecations

Perintah ini mencatat semua pelanggaran privasi dan dependensi yang ada saat ini ke dalam file YAML per paket. CI akan tetap berstatus hijau (pass) selama pengembang tidak menambah pelanggaran baru.

Metrik dan Tata Kelola Utang Teknis

Pantau penurunan pelanggaran dengan script sederhana yang menghitung jumlah entri pada file deprecation:

# Script verifikasi tren utang teknis
violations=$(grep -r "violations:" packages/*/deprecated_references.yml | wc -l)
echo "Total domain violations: $violations"

Aturan kerja tim:

  1. Strict No-New-Violations: PR yang memunculkan entri baru di deprecated_references.yml otomatis ditolak di CI.
  2. Boy Scout Rule: Setiap tugas pemeliharaan pada domain tertentu wajib mengurangi minimal 1 referensi terdepresiasi.
  3. API Inversion: Jika paket A membutuhkan data dari paket B secara intensif, gunakan event/pub-sub asinkron alih-alih menambahkan ketergantungan langsung.