๐๏ธ Hexagonal Architecture dari Nol#
๐ Apa itu Hexagonal Architecture?#
Hexagonal Architecture (disebut juga Ports and Adapters) adalah cara mengorganisir kode aplikasi kita agar: - Business logic (logika bisnis) terpisah dari teknologi - Mudah diubah dan di-test - Tidak bergantung pada database, framework, atau API tertentu
๐ค Analogi Sederhana: Rumah dengan Banyak Pintu#
Bayangkan aplikasi kita adalah sebuah rumah (domain/business logic):
๐ช Pintu Depan (HTTP API)
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
โ ๐ RUMAH (DOMAIN) โ โ Inti aplikasi kita
โ Business Logic โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
๐ช Pintu Samping ๐ช Pintu Belakang
(CLI) (Database)
Konsep Utama: - Rumah (Domain) = Business logic kita (aturan bisnis, entitas) - Pintu-pintu (Ports) = Interface/kontrak untuk masuk/keluar - Kunci Pintu (Adapters) = Implementasi konkret (PostgreSQL, FastAPI, dll)
Kenapa berbentuk Hexagon (Segi Enam)? Bukan karena harus 6 sisi, tapi untuk menunjukkan bahwa aplikasi bisa punya banyak pintu masuk dan keluar dari berbagai arah!
โ Mengapa Menggunakan Hexagonal Architecture?#
๐ด Masalah Tanpa Hexagonal Architecture#
Bayangkan kita bikin aplikasi toko online dengan cara biasa:
# โ Kode campur aduk (tanpa hexagonal)
from fastapi import FastAPI
from sqlalchemy import create_engine
app = FastAPI()
@app.post("/order")
def create_order(item: str, qty: int):
# Business logic CAMPUR dengan teknologi!
# 1. Validasi (business logic)
if qty <= 0:
return {"error": "Quantity harus positif"}
# 2. Hitung harga (business logic)
price = qty * 10000
# 3. Simpan ke database (teknologi - PostgreSQL)
engine = create_engine("postgresql://...")
engine.execute(f"INSERT INTO orders VALUES ('{item}', {qty}, {price})")
# 4. Kirim email (teknologi - SMTP)
send_email_via_smtp("order@toko.com", f"Order baru: {item}")
return {"success": True, "total": price}
๐จ Masalahnya: 1. โ Sulit di-test - Harus punya database dan email server untuk testing 2. โ Sulit diubah - Mau ganti dari PostgreSQL ke MongoDB? Harus ubah semua kode! 3. โ Kode berantakan - Business logic (hitung harga) campur dengan database 4. โ Sulit dikembangkan tim - Semua orang harus nunggu setup database
โ Solusi: Dengan Hexagonal Architecture#
Kita pisahkan menjadi:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ INTERFACE (Pintu Masuk) โ
โ - FastAPI Controller โ
โ - CLI Command โ
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ APPLICATION (Use Cases) โ
โ - CreateOrderUseCase โ
โ - SendOrderNotification โ
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ DOMAIN (Business Logic) ๐ โ
โ - Order Entity โ
โ - Hitung total harga โ
โ - Validasi quantity โ
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ INFRASTRUCTURE (Pintu Keluar) โ
โ - PostgreSQLRepository โ
โ - EmailService โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
๐ฏ Keuntungan: 1. โ Mudah di-test - Test business logic tanpa database! 2. โ Mudah diubah - Ganti PostgreSQL ke MongoDB? Tinggal ganti 1 file! 3. โ Kode rapi - Setiap layer punya tanggung jawab jelas 4. โ Tim bisa kerja paralel - Developer A bikin API, Developer B bikin business logic
๐๏ธ Cara Menggunakan Hexagonal Architecture#
๐ 4 Layer Utama#
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 1. INTERFACE (Delivery/Driving Adapters) โ
โ Cara user berinteraksi dengan aplikasi โ
โ - HTTP/REST API (FastAPI, Flask) โ
โ - CLI (Command Line) โ
โ - GraphQL โ
โ - WebSocket โ
โโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ (memanggil)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 2. APPLICATION (Use Cases) โ
โ Apa yang bisa dilakukan aplikasi โ
โ - CreateOrderUseCase โ
โ - GetOrderUseCase โ
โ - UpdateOrderUseCase โ
โ - DeleteOrderUseCase โ
โโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ (menggunakan)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 3. DOMAIN (Business Logic) ๐ INTI APLIKASI โ
โ Aturan bisnis & data penting โ
โ - Order (entity) โ
โ - Product (entity) โ
โ - Hitung diskon โ
โ - Validasi stok โ
โ - Repository Interface (kontrak) โ
โโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ (diimplementasi oleh)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 4. INFRASTRUCTURE (Driven Adapters) โ
โ Implementasi teknologi eksternal โ
โ - PostgreSQLRepository โ
โ - MongoDBRepository โ
โ - EmailService (SMTP) โ
โ - PaymentGateway (Midtrans) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
๐ก Contoh Lengkap: Aplikasi Toko Online#
1๏ธโฃ DOMAIN Layer (Inti Bisnis - Yang Paling Penting!)#
# domain/entities/order.py
from dataclasses import dataclass
from datetime import datetime
@dataclass
class Order:
"""Entity Order - Objek bisnis utama"""
id: str
product_name: str
quantity: int
price_per_item: int
created_at: datetime
def calculate_total(self) -> int:
"""Business logic: Hitung total harga"""
return self.quantity * self.price_per_item
def apply_discount(self, percentage: int) -> int:
"""Business logic: Hitung diskon"""
total = self.calculate_total()
discount = total * percentage / 100
return total - discount
def validate(self) -> bool:
"""Business logic: Validasi order"""
if self.quantity <= 0:
raise ValueError("Quantity harus lebih dari 0")
if self.price_per_item <= 0:
raise ValueError("Harga harus lebih dari 0")
return True
# domain/repositories/order_repository.py
from abc import ABC, abstractmethod
from typing import List, Optional
from domain.entities.order import Order
class OrderRepository(ABC):
"""PORT (Interface) - Kontrak untuk simpan/ambil Order"""
@abstractmethod
def save(self, order: Order) -> Order:
"""Simpan order ke database"""
pass
@abstractmethod
def find_by_id(self, order_id: str) -> Optional[Order]:
"""Cari order by ID"""
pass
@abstractmethod
def find_all(self) -> List[Order]:
"""Ambil semua order"""
pass
๐ Poin Penting Domain:
- โ
Tidak ada import dari FastAPI, SQLAlchemy, atau teknologi lain!
- โ
Pure Python - hanya business logic
- โ
Mudah di-test - tidak butuh database untuk test calculate_total()
2๏ธโฃ APPLICATION Layer (Use Cases)#
# application/usecases/create_order.py
from datetime import datetime
from domain.entities.order import Order
from domain.repositories.order_repository import OrderRepository
class CreateOrderUseCase:
"""Use Case: Buat order baru"""
def __init__(self, order_repository: OrderRepository):
# Dependency Injection - terima interface, bukan implementasi!
self.order_repository = order_repository
def execute(self, product_name: str, quantity: int, price: int) -> Order:
"""Jalankan use case"""
# 1. Buat entity Order
order = Order(
id=self._generate_id(),
product_name=product_name,
quantity=quantity,
price_per_item=price,
created_at=datetime.now()
)
# 2. Validasi (business logic)
order.validate()
# 3. Simpan menggunakan repository (interface)
saved_order = self.order_repository.save(order)
# 4. Return
return saved_order
def _generate_id(self) -> str:
import uuid
return str(uuid.uuid4())
๐ Poin Penting Application: - โ Hanya depend pada Domain (import dari domain/) - โ Terima interface, bukan implementasi konkret - โ Satu use case = satu aksi bisnis
3๏ธโฃ INFRASTRUCTURE Layer (Implementasi Teknologi)#
# infrastructure/database/postgresql_order_repository.py
from typing import List, Optional
from sqlalchemy import create_engine, Column, String, Integer, DateTime
from sqlalchemy.orm import declarative_base, Session
from domain.entities.order import Order
from domain.repositories.order_repository import OrderRepository
Base = declarative_base()
class OrderModel(Base):
"""ORM Model untuk PostgreSQL"""
__tablename__ = "orders"
id = Column(String, primary_key=True)
product_name = Column(String)
quantity = Column(Integer)
price_per_item = Column(Integer)
created_at = Column(DateTime)
class PostgreSQLOrderRepository(OrderRepository):
"""ADAPTER - Implementasi OrderRepository untuk PostgreSQL"""
def __init__(self, database_url: str):
self.engine = create_engine(database_url)
Base.metadata.create_all(self.engine)
def save(self, order: Order) -> Order:
"""Simpan ke PostgreSQL"""
with Session(self.engine) as session:
order_model = OrderModel(
id=order.id,
product_name=order.product_name,
quantity=order.quantity,
price_per_item=order.price_per_item,
created_at=order.created_at
)
session.add(order_model)
session.commit()
return order
def find_by_id(self, order_id: str) -> Optional[Order]:
"""Cari dari PostgreSQL"""
with Session(self.engine) as session:
order_model = session.query(OrderModel).filter(
OrderModel.id == order_id
).first()
if not order_model:
return None
# Convert ORM model ke Domain entity
return Order(
id=order_model.id,
product_name=order_model.product_name,
quantity=order_model.quantity,
price_per_item=order_model.price_per_item,
created_at=order_model.created_at
)
def find_all(self) -> List[Order]:
"""Ambil semua dari PostgreSQL"""
with Session(self.engine) as session:
order_models = session.query(OrderModel).all()
return [
Order(
id=om.id,
product_name=om.product_name,
quantity=om.quantity,
price_per_item=om.price_per_item,
created_at=om.created_at
)
for om in order_models
]
๐ก Mau ganti ke MongoDB? Bikin adapter baru!
# infrastructure/database/mongodb_order_repository.py
from pymongo import MongoClient
from domain.repositories.order_repository import OrderRepository
class MongoDBOrderRepository(OrderRepository):
"""ADAPTER - Implementasi OrderRepository untuk MongoDB"""
def __init__(self, connection_string: str):
self.client = MongoClient(connection_string)
self.db = self.client.orders_db
self.collection = self.db.orders
def save(self, order: Order) -> Order:
"""Simpan ke MongoDB"""
self.collection.insert_one({
"id": order.id,
"product_name": order.product_name,
"quantity": order.quantity,
"price_per_item": order.price_per_item,
"created_at": order.created_at
})
return order
# ... implementasi lainnya
๐ Poin Penting Infrastructure: - โ Implementasi interface dari Domain - โ Semua teknologi (database, API eksternal, email) ada di sini - โ Ganti teknologi mudah - tinggal ganti adapter!
4๏ธโฃ INTERFACE Layer (API/CLI)#
# interface/http/order_controller.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from application.usecases.create_order import CreateOrderUseCase
from infrastructure.database.postgresql_order_repository import PostgreSQLOrderRepository
app = FastAPI()
# Setup dependency (bisa pakai Dependency Injection framework)
order_repository = PostgreSQLOrderRepository("postgresql://localhost/orders")
create_order_use_case = CreateOrderUseCase(order_repository)
class CreateOrderRequest(BaseModel):
product_name: str
quantity: int
price: int
@app.post("/orders")
def create_order(request: CreateOrderRequest):
"""HTTP Endpoint untuk buat order"""
try:
# Panggil use case
order = create_order_use_case.execute(
product_name=request.product_name,
quantity=request.quantity,
price=request.price
)
# Return response
return {
"id": order.id,
"product": order.product_name,
"quantity": order.quantity,
"total": order.calculate_total()
}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
CLI Alternative:
# interface/cli/order_cli.py
import click
from application.usecases.create_order import CreateOrderUseCase
from infrastructure.database.postgresql_order_repository import PostgreSQLOrderRepository
order_repository = PostgreSQLOrderRepository("postgresql://localhost/orders")
create_order_use_case = CreateOrderUseCase(order_repository)
@click.command()
@click.option('--product', prompt='Nama produk')
@click.option('--quantity', prompt='Jumlah', type=int)
@click.option('--price', prompt='Harga', type=int)
def create_order_cli(product: str, quantity: int, price: int):
"""CLI Command untuk buat order"""
order = create_order_use_case.execute(product, quantity, price)
click.echo(f"โ
Order berhasil dibuat! ID: {order.id}")
click.echo(f"Total: Rp {order.calculate_total():,}")
๐ Poin Penting Interface: - โ Hanya handle request/response - tidak ada business logic! - โ Panggil use case untuk eksekusi bisnis - โ Bisa punya banyak interface (HTTP, CLI, GraphQL) untuk use case yang sama
๐งช Testing Jadi Mudah!#
Test Domain (Tanpa Database!)#
# test/domain/test_order.py
from domain.entities.order import Order
from datetime import datetime
def test_calculate_total():
"""Test business logic tanpa database!"""
order = Order(
id="1",
product_name="Laptop",
quantity=2,
price_per_item=5000000,
created_at=datetime.now()
)
assert order.calculate_total() == 10000000 # 2 x 5 juta
def test_apply_discount():
order = Order(
id="1",
product_name="Laptop",
quantity=2,
price_per_item=5000000,
created_at=datetime.now()
)
# Diskon 10%
final_price = order.apply_discount(10)
assert final_price == 9000000 # 10 juta - 10%
Test Use Case dengan Mock#
# test/application/test_create_order.py
from unittest.mock import Mock
from application.usecases.create_order import CreateOrderUseCase
def test_create_order():
"""Test use case dengan mock repository"""
# Mock repository (fake, bukan database beneran)
mock_repo = Mock()
mock_repo.save.return_value = Mock(id="123")
# Test use case
use_case = CreateOrderUseCase(mock_repo)
order = use_case.execute("Laptop", 2, 5000000)
# Verify
assert mock_repo.save.called
assert order is not None
๐ Struktur Folder yang Benar#
src/
โโโ domain/ # ๐ INTI (Paling Penting!)
โ โโโ entities/ # Objek bisnis
โ โ โโโ order.py
โ โ โโโ product.py
โ โ โโโ user.py
โ โโโ value_objects/ # Konsep immutable
โ โ โโโ email.py
โ โ โโโ money.py
โ โโโ services/ # Logic bisnis kompleks
โ โ โโโ pricing_service.py
โ โโโ repositories/ # Interface (kontrak)
โ โโโ order_repository.py
โ โโโ product_repository.py
โ
โโโ application/ # ๐ฏ Use Cases
โ โโโ usecases/
โ โ โโโ create_order.py
โ โ โโโ get_order.py
โ โ โโโ update_order.py
โ โโโ dto/ # Data transfer objects
โ โ โโโ order_dto.py
โ โ โโโ product_dto.py
โ โโโ services/ # Application services
โ โโโ order_service.py
โ
โโโ infrastructure/ # ๐ง Implementasi Teknologi
โ โโโ database/
โ โ โโโ postgresql_order_repository.py
โ โ โโโ mongodb_product_repository.py
โ โโโ external/
โ โ โโโ midtrans_payment.py
โ โ โโโ smtp_email_service.py
โ โโโ ai/
โ โโโ openai_service.py
โ
โโโ interface/ # ๐ API/CLI
โโโ http/
โ โโโ order_controller.py
โ โโโ product_controller.py
โโโ cli/
โโโ order_cli.py
๐ฏ Aturan Emas Hexagonal Architecture#
โ BOLEH:#
- โ Interface boleh import Application
- โ Application boleh import Domain
- โ Infrastructure boleh import Domain (untuk implement interface)
- โ Infrastructure boleh import Application (untuk implement ports)
โ TIDAK BOLEH:#
- โ Domain TIDAK BOLEH import Application
- โ Domain TIDAK BOLEH import Infrastructure
- โ Domain TIDAK BOLEH import Interface
- โ Application TIDAK BOLEH import Infrastructure
- โ Application TIDAK BOLEH import Interface
Contoh Salah:
# โ SALAH! Domain tidak boleh import SQLAlchemy
# domain/entities/order.py
from sqlalchemy import Column, Integer # โ TIDAK BOLEH!
class Order:
id = Column(Integer) # โ TIDAK BOLEH!
Contoh Benar:
# โ
BENAR! Domain pure Python
# domain/entities/order.py
from dataclasses import dataclass
@dataclass
class Order: # โ
Pure Python!
id: str
quantity: int
๐ Langkah-langkah Implementasi#
Step 1: Mulai dari Domain (INTI)#
- Identifikasi objek bisnis (Entity)
- Order
- Product
-
User
-
Tulis business logic
- Validasi
- Perhitungan
-
Aturan bisnis
-
Buat interface repository
- Kontrak untuk save/find data
Step 2: Buat Use Cases (Application)#
- Identifikasi aksi user
- Create order
- Cancel order
-
Get order
-
Implementasi use case
- Panggil domain entity
- Gunakan repository interface
Step 3: Implementasi Teknologi (Infrastructure)#
- Pilih database (PostgreSQL/MongoDB/dll)
- Implementasi repository interface
- Setup connection
- Implementasi external services
Step 4: Buat Interface (API/CLI)#
- Pilih delivery mechanism (FastAPI/Flask/CLI)
- Buat controller/handler
- Panggil use case
- Return response
๐ก Tips untuk Pemula#
1. Mulai Sederhana#
Jangan langsung kompleks! Mulai dengan 1 entity, 1 use case, 1 endpoint.
2. Domain Dulu, Teknologi Kemudian#
Pikirkan business logic dulu, baru teknologi (database, API, dll).
3. Test Business Logic Tanpa Database#
Ini keuntungan terbesar! Test tanpa setup database.
4. Jangan Takut Refactor#
Ganti PostgreSQL ke MongoDB? Tinggal ganti 1 file di infrastructure!
5. Konsisten dengan Aturan#
Domain tidak boleh tahu tentang FastAPI, SQLAlchemy, atau teknologi lain!
๐ Kesimpulan#
Hexagonal Architecture = Pisahkan Business Logic dari Teknologi
- Domain = Aturan bisnis (inti aplikasi) ๐
- Application = Apa yang bisa dilakukan aplikasi ๐ฏ
- Infrastructure = Implementasi teknologi (database, email, dll) ๐ง
- Interface = Cara user akses aplikasi (API, CLI, dll) ๐
Keuntungan: - โ Mudah di-test - โ Mudah diubah - โ Kode rapi dan terorganisir - โ Tim bisa kerja paralel - โ Independent dari framework/database
Ingat: Domain adalah RAJA! Semua layer lain melayani domain! ๐