Memuat...
👋 Selamat Pagi!

Cara Membangun Arsitektur Hexagonal untuk Aplikasi yang Mudah Diuji

Pelajari arsitektur hexagonal yang membuat aplikasi Anda fleksibel, mudah diuji, dan tahan perubahan teknologi. Panduan lengkap dengan contoh praktis untuk deve...

Cara Membangun Arsitektur Hexagonal untuk Aplikasi yang Mudah Diuji

Pernah mengalami nightmare saat harus mengganti database atau framework di tengah project? Atau kesulitan menulis unit test karena kode terlalu bergantung pada library eksternal?

Masalah ini sangat umum dialami developer, terutama ketika aplikasi berkembang dan requirements berubah drastis.

Arsitektur Hexagonal atau yang dikenal sebagai Ports and Adapters pattern hadir sebagai solusi elegant untuk masalah tersebut.

Pattern ini pertama kali diperkenalkan oleh Alistair Cockburn pada tahun 2005 dan kini menjadi fondasi banyak aplikasi enterprise modern.

Apa Itu Arsitektur Hexagonal?

Bayangkan aplikasi Anda seperti sebuah hexagon (segi enam) yang berada di tengah, dikelilingi oleh berbagai sistem eksternal seperti database, API, user interface, atau message queue.

Inti dari aplikasi (business logic) tidak boleh tahu atau peduli dengan teknologi apa yang digunakan di luar.

Aplikasi hanya mendefinisikan "ports" (interface/contract) untuk komunikasi, sementara "adapters" bertanggung jawab mengimplementasikan detail teknisnya.

Dengan begitu, Anda bisa mengganti MySQL dengan PostgreSQL, REST API dengan GraphQL, atau bahkan framework tanpa menyentuh business logic.

Mengapa Developer Indonesia Perlu Mempelajari Ini?

Di Indonesia, banyak project yang dimulai dengan tech stack sederhana lalu tiba-tiba harus scale up atau pivot.

Startup yang awalnya pakai shared hosting tiba-tiba butuh migrasi ke cloud infrastructure.

Atau perusahaan yang tadinya pakai monolith terpaksa harus pecah jadi microservices.

Tanpa arsitektur yang baik, perubahan ini bisa memakan waktu berbulan-bulan dan menghasilkan bug dimana-mana.

Arsitektur Hexagonal membantu Anda membangun aplikasi yang siap menghadapi perubahan sejak hari pertama.

Komponen Utama Arsitektur Hexagonal

1. Domain Layer (Inti Aplikasi)

Ini adalah jantung aplikasi yang berisi business logic murni tanpa ketergantungan apapun.

Domain layer hanya fokus pada aturan bisnis, entities, dan use cases.

Misalnya untuk aplikasi e-commerce, domain layer menangani logika seperti "stok produk harus dikurangi saat order dibuat" atau "diskon hanya berlaku untuk member premium".

// Domain Entity - Pure PHP, no framework dependency
class Order
{
    private string $id;
    private array $items;
    private string $status;
    private float $totalAmount;

    public function __construct(string $id)
    {
        $this->id = $id;
        $this->items = [];
        $this->status = 'pending';
        $this->totalAmount = 0;
    }

    public function addItem(Product $product, int $quantity): void
    {
        if ($quantity isAvailable($quantity)) {
            throw new OutOfStockException('Product out of stock');
        }

        $this->items[] = new OrderItem($product, $quantity);
        $this->calculateTotal();
    }

    public function confirm(): void
    {
        if (empty($this->items)) {
            throw new EmptyOrderException('Cannot confirm empty order');
        }

        $this->status = 'confirmed';
    }

    private function calculateTotal(): void
    {
        $this->totalAmount = array_reduce(
            $this->items,
            fn($sum, $item) => $sum + $item->getSubtotal(),
            0
        );
    }
}

Perhatikan bahwa class Order di atas tidak menggunakan annotation, tidak extend framework tertentu, dan tidak ada database query.

Ini murni business logic yang bisa ditest dengan mudah tanpa perlu setup database atau framework.

2. Ports (Interface/Contract)

Ports adalah interface yang mendefinisikan bagaimana domain berkomunikasi dengan dunia luar.

Ada dua jenis ports: input ports (driving) dan output ports (driven).

Input Ports adalah interface untuk use cases yang dipanggil dari luar (controller, CLI, event listener).

// Input Port - Use Case Interface
interface CreateOrderUseCase
{
    public function execute(CreateOrderRequest $request): OrderResponse;
}

// Implementation
class CreateOrder implements CreateOrderUseCase
{
    private OrderRepository $orderRepository;
    private ProductRepository $productRepository;
    private PaymentGateway $paymentGateway;

    public function __construct(
        OrderRepository $orderRepository,
        ProductRepository $productRepository,
        PaymentGateway $paymentGateway
    ) {
        $this->orderRepository = $orderRepository;
        $this->productRepository = $productRepository;
        $this->paymentGateway = $paymentGateway;
    }

    public function execute(CreateOrderRequest $request): OrderResponse
    {
        $order = new Order($this->generateOrderId());

        foreach ($request->getItems() as $item) {
            $product = $this->productRepository->findById($item['product_id']);
            $order->addItem($product, $item['quantity']);
        }

        $order->confirm();
        
        $this->orderRepository->save($order);
        $this->paymentGateway->createPayment($order);

        return new OrderResponse($order);
    }

    private function generateOrderId(): string
    {
        return 'ORD-' . time() . '-' . rand(1000, 9999);
    }
}

Output Ports adalah interface untuk dependencies seperti database, email service, atau payment gateway.

// Output Port - Repository Interface
interface OrderRepository
{
    public function save(Order $order): void;
    public function findById(string $id): ?Order;
    public function findByCustomerId(string $customerId): array;
}

// Output Port - Payment Gateway Interface
interface PaymentGateway
{
    public function createPayment(Order $order): Payment;
    public function checkStatus(string $paymentId): string;
}

Dengan mendefinisikan interface seperti ini, domain layer tidak perlu tahu apakah Anda pakai MySQL, MongoDB, atau in-memory storage.

3. Adapters (Implementasi Teknis)

Adapters adalah implementasi konkret dari ports yang berinteraksi dengan teknologi spesifik.

Input Adapters menerima request dari luar dan memanggil use case.

// HTTP Controller Adapter (Laravel example)
class OrderController extends Controller
{
    private CreateOrderUseCase $createOrderUseCase;

    public function __construct(CreateOrderUseCase $createOrderUseCase)
    {
        $this->createOrderUseCase = $createOrderUseCase;
    }

    public function store(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'items' => 'required|array',
            'items.*.product_id' => 'required|string',
            'items.*.quantity' => 'required|integer|min:1'
        ]);

        try {
            $createOrderRequest = new CreateOrderRequest(
                $request->user()->id,
                $validated['items']
            );

            $response = $this->createOrderUseCase->execute($createOrderRequest);

            return response()->json([
                'success' => true,
                'order_id' => $response->getOrderId(),
                'total' => $response->getTotal()
            ], 201);

        } catch (OutOfStockException $e) {
            return response()->json([
                'success' => false,
                'message' => $e->getMessage()
            ], 400);
        }
    }
}

Output Adapters mengimplementasikan interface repository atau gateway dengan teknologi spesifik.

// Database Adapter using Eloquent
class EloquentOrderRepository implements OrderRepository
{
    public function save(Order $order): void
    {
        $orderModel = OrderModel::find($order->getId()) ?? new OrderModel();
        
        $orderModel->id = $order->getId();
        $orderModel->status = $order->getStatus();
        $orderModel->total_amount = $order->getTotalAmount();
        $orderModel->save();

        // Save order items
        foreach ($order->getItems() as $item) {
            OrderItemModel::create([
                'order_id' => $order->getId(),
                'product_id' => $item->getProductId(),
                'quantity' => $item->getQuantity(),
                'price' => $item->getPrice()
            ]);
        }
    }

    public function findById(string $id): ?Order
    {
        $orderModel = OrderModel::with('items')->find($id);
        
        if (!$orderModel) {
            return null;
        }

        return $this->mapToDomain($orderModel);
    }

    private function mapToDomain(OrderModel $model): Order
    {
        $order = new Order($model->id);
        // Reconstruct domain object from database model
        // ... mapping logic
        return $order;
    }
}

Kesulitan dengan tugas programming atau butuh bantuan coding? KerjaKode siap membantu menyelesaikan tugas IT dan teknik informatika Anda. Dapatkan bantuan profesional di jasa tugas IT KerjaKode.

Keuntungan Menggunakan Arsitektur Hexagonal

1. Testability yang Luar Biasa

Karena business logic tidak bergantung pada framework atau database, Anda bisa menulis unit test yang cepat dan reliable.

// Unit Test - No database, no framework needed
class CreateOrderTest extends TestCase
{
    public function test_create_order_success()
    {
        // Mock repositories
        $orderRepo = $this->createMock(OrderRepository::class);
        $productRepo = $this->createMock(ProductRepository::class);
        $paymentGateway = $this->createMock(PaymentGateway::class);

        // Setup mock behavior
        $product = new Product('PROD-1', 'Laptop', 10000000, 5);
        $productRepo->method('findById')->willReturn($product);

        $useCase = new CreateOrder($orderRepo, $productRepo, $paymentGateway);

        $request = new CreateOrderRequest('USER-1', [
            ['product_id' => 'PROD-1', 'quantity' => 2]
        ]);

        $response = $useCase->execute($request);

        $this->assertNotNull($response->getOrderId());
        $this->assertEquals(20000000, $response->getTotal());
    }
}

Test seperti ini bisa running dalam milidetik karena tidak ada I/O operations.

2. Fleksibilitas Teknologi

Anda bisa mulai dengan SQLite untuk development, pakai MySQL di staging, dan PostgreSQL di production tanpa mengubah business logic.

Bahkan bisa bikin adapter in-memory untuk testing atau demo.

// In-Memory Adapter for Testing
class InMemoryOrderRepository implements OrderRepository
{
    private array $orders = [];

    public function save(Order $order): void
    {
        $this->orders[$order->getId()] = $order;
    }

    public function findById(string $id): ?Order
    {
        return $this->orders[$id] ?? null;
    }

    public function clear(): void
    {
        $this->orders = [];
    }
}

3. Parallel Development

Tim bisa bekerja parallel dengan efisien karena dependency yang jelas.

Backend engineer bisa fokus ke business logic, sementara frontend engineer pakai mock data.

Database admin bisa optimize schema tanpa ganggu business logic.

4. Maintenance yang Mudah

Ketika ada bug atau perubahan business rule, Anda tahu persis dimana harus mengubah code.

Business logic ada di domain layer, database logic ada di repository adapter, API logic ada di controller adapter.

Implementasi Praktis di Laravel

Mari kita lihat struktur folder yang recommended untuk arsitektur hexagonal di Laravel.

app/
├── Domain/                    # Core business logic
│   ├── Order/
│   │   ├── Entity/
│   │   │   ├── Order.php
│   │   │   └── OrderItem.php
│   │   ├── Exception/
│   │   │   ├── OutOfStockException.php
│   │   │   └── EmptyOrderException.php
│   │   └── ValueObject/
│   │       └── Money.php
│   └── Product/
│       └── Entity/
│           └── Product.php
│
├── Application/               # Use cases (ports)
│   ├── Order/
│   │   ├── CreateOrder/
│   │   │   ├── CreateOrderUseCase.php
│   │   │   ├── CreateOrder.php
│   │   │   ├── CreateOrderRequest.php
│   │   │   └── OrderResponse.php
│   │   └── GetOrder/
│   │       ├── GetOrderUseCase.php
│   │       └── GetOrder.php
│   └── Port/
│       ├── OrderRepository.php
│       ├── ProductRepository.php
│       └── PaymentGateway.php
│
└── Infrastructure/            # Adapters
    ├── Http/
    │   └── Controller/
    │       └── OrderController.php
    ├── Persistence/
    │   ├── Eloquent/
    │   │   ├── OrderModel.php
    │   │   └── EloquentOrderRepository.php
    │   └── InMemory/
    │       └── InMemoryOrderRepository.php
    └── Payment/
        ├── MidtransPaymentGateway.php
        └── XenditPaymentGateway.php

Dependency Injection Setup

Di Laravel, Anda perlu mendaftarkan binding di Service Provider.

// app/Providers/AppServiceProvider.php
class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        // Bind repository interfaces to implementations
        $this->app->bind(
            OrderRepository::class,
            EloquentOrderRepository::class
        );

        $this->app->bind(
            ProductRepository::class,
            EloquentProductRepository::class
        );

        // Bind use cases
        $this->app->bind(
            CreateOrderUseCase::class,
            CreateOrder::class
        );

        // Bind payment gateway based on config
        $this->app->bind(PaymentGateway::class, function ($app) {
            $gateway = config('payment.default_gateway');
            
            return match($gateway) {
                'midtrans' => new MidtransPaymentGateway(
                    config('payment.midtrans.server_key')
                ),
                'xendit' => new XenditPaymentGateway(
                    config('payment.xendit.api_key')
                ),
                default => throw new Exception('Invalid gateway')
            };
        });
    }
}

Dengan setup seperti ini, Anda bisa switch payment gateway hanya dengan mengubah config file.

Common Pitfalls dan Cara Menghindarinya

1. Anemic Domain Model

Jangan buat entity yang hanya punya getter/setter tanpa behavior.

Domain entity harus punya business logic dan enforce business rules.

// BAD - Anemic model
class Order
{
    public string $status;
    public float $total;
    
    public function getStatus() { return $this->status; }
    public function setStatus($status) { $this->status = $status; }
}

// GOOD - Rich domain model
class Order
{
    private string $status;
    private float $total;
    
    public function confirm(): void
    {
        if ($this->status !== 'pending') {
            throw new InvalidStateException('Only pending orders can be confirmed');
        }
        
        $this->status = 'confirmed';
        $this->recordEvent(new OrderConfirmedEvent($this->id));
    }
}

2. Leaking Persistence Details

Jangan biarkan Eloquent Model atau database details bocor ke domain layer.

Selalu mapping dari persistence model ke domain entity.

3. Over-Engineering

Tidak semua aplikasi butuh arsitektur kompleks.

Untuk CRUD sederhana atau prototype, Laravel standar sudah cukup.

Gunakan hexagonal architecture ketika aplikasi punya business logic kompleks atau expected untuk scale dan berubah signifikan.

Testing Strategy

Arsitektur hexagonal memungkinkan testing pyramid yang ideal.

// Unit Test - Domain logic (fast, many)
class OrderTest extends TestCase
{
    public function test_cannot_add_zero_quantity()
    {
        $order = new Order('ORD-1');
        $product = new Product('PROD-1', 'Item', 10000, 5);
        
        $this->expectException(InvalidArgumentException::class);
        $order->addItem($product, 0);
    }
}

// Integration Test - Use case with mocked adapters (medium speed, fewer)
class CreateOrderIntegrationTest extends TestCase
{
    public function test_create_order_flow()
    {
        $orderRepo = new InMemoryOrderRepository();
        $productRepo = new InMemoryProductRepository();
        $paymentGateway = new FakePaymentGateway();
        
        // Setup test data
        $productRepo->save(new Product('PROD-1', 'Laptop', 10000000, 5));
        
        $useCase = new CreateOrder($orderRepo, $productRepo, $paymentGateway);
        $request = new CreateOrderRequest('USER-1', [
            ['product_id' => 'PROD-1', 'quantity' => 1]
        ]);
        
        $response = $useCase->execute($request);
        
        $this->assertNotNull($response->getOrderId());
        $savedOrder = $orderRepo->findById($response->getOrderId());
        $this->assertEquals('confirmed', $savedOrder->getStatus());
    }
}

// E2E Test - Full HTTP request (slow, few)
class OrderApiTest extends TestCase
{
    public function test_create_order_via_api()
    {
        $response = $this->postJson('/api/orders', [
            'items' => [
                ['product_id' => 'PROD-1', 'quantity' => 1]
            ]
        ]);
        
        $response->assertStatus(201)
                 ->assertJsonStructure(['order_id', 'total']);
    }
}

Migration Strategy untuk Project Existing

Tidak perlu rewrite seluruh aplikasi sekaligus.

Mulai dengan mengekstrak satu feature atau module yang sering berubah atau bermasalah.

Step 1: Identifikasi business logic yang tercampur dengan framework code.

Step 2: Buat domain entity dan use case baru di folder terpisah.

Step 3: Buat interface repository dan implementasinya.

Step 4: Refactor controller untuk menggunakan use case instead of direct model access.

Step 5: Tulis unit test untuk domain logic yang baru.

Step 6: Ulangi untuk feature lain secara bertahap.

Tools dan Resources

Beberapa package yang bisa membantu implementasi:

  • PHPUnit - Testing framework untuk unit dan integration test
  • Mockery - Mocking library untuk test doubles
  • PHP-DI - Dependency injection container alternatif
  • Doctrine - ORM yang mendukung Data Mapper pattern (alternatif Eloquent)
  • Tactician - Command bus untuk use case pattern

Kesimpulan

Arsitektur Hexagonal memberikan foundation yang solid untuk aplikasi yang scalable dan maintainable.

Dengan memisahkan business logic dari technical details, Anda mendapatkan kode yang mudah ditest, fleksibel terhadap perubahan teknologi, dan lebih mudah dipahami tim.

Pattern ini mungkin terlihat overhead di awal, tapi investasi waktu akan terbayar ketika aplikasi berkembang dan requirements berubah.

Mulai dari satu module, rasakan benefitnya, lalu expand ke bagian lain dari aplikasi.

Yang terpenting adalah konsistensi dan discipline dalam menerapkan boundaries antara layers.

Dengan arsitektur yang baik, Anda bisa fokus membangun fitur baru instead of firefighting bugs atau takut melakukan perubahan.

Selamat mencoba dan semoga aplikasi Anda menjadi lebih robust dan future-proof!

Ajie Kusumadhany
Written by

Ajie Kusumadhany

Founder & Lead Developer KerjaKode. Berpengalaman dalam pengembangan web modern dengan Laravel, React.js, Vue.js, dan teknologi terkini. Passionate tentang coding, teknologi, dan berbagi pengetahuan melalui artikel.

Promo Spesial Hari Ini!

10% DISKON

Promo berakhir dalam:

00 Jam
:
00 Menit
:
00 Detik
Klaim Promo Sekarang!

*Promo berlaku untuk order hari ini

0
User Online
Halo! 👋
Kerjakode Support Online
×

👋 Hai! Pilih layanan yang kamu butuhkan:

Chat WhatsApp Sekarang