Hexagonal Architecture: Memahami Ports dan Adapters

SOFTWARE ARCHITECTURE By TryzTech Team
Hexagonal ArchitectureSoftware ArchitectureBackendTestingClean Architecture
Bagikan

Daftar Isi

Pendahuluan

Banyak aplikasi menjadi sulit diubah karena business logic tercampur dengan framework, database, HTTP handler, message queue, dan SDK pihak ketiga.

Hexagonal architecture, atau ports and adapters, mencoba melindungi core aplikasi dari detail eksternal seperti itu.

Tujuannya bukan membuat folder dengan nama keren. Tujuannya adalah menjaga business rules tetap mudah dites, diganti, dan dipahami.

Ide Utamanya

Hexagonal architecture menempatkan application core di tengah.

Dunia luar berbicara ke core lewat ports. Adapters menghubungkan ports tersebut ke teknologi nyata.

HTTP Controller -> Input Port -> Application Service -> Output Port -> Database Adapter

Core tidak perlu peduli request datang dari REST, GraphQL, CLI, atau message queue. Core cukup peduli pada use case.

Ports dan Adapters

Port adalah interface yang menjelaskan apa yang dibutuhkan atau ditawarkan aplikasi.

Adapter menghubungkan port itu ke tool nyata.

KonsepContoh
Input portRegisterUserUseCase
Input adapterHTTP controller, CLI command
Output portUserRepository, EmailSender
Output adapterPostgreSQL repository, SMTP sender

Pemisahan ini membuat teknologi lebih mudah diganti tanpa menulis ulang business rules.

Arah Dependency

Aturan paling penting adalah arah dependency.

Core boleh mendefinisikan interface. Adapter eksternal mengimplementasikannya. Core sebaiknya tidak mengimpor detail framework atau infrastructure.

Arah yang buruk:

Application service -> Express request -> Database client

Arah yang lebih baik:

Controller -> Application service -> Repository port
Postgres adapter -> Repository port

Application service bergantung pada abstraction. Infrastructure bergantung pada kontrak aplikasi.

Contoh: User Registration

Bayangkan use case registrasi user.

Core perlu:

  • memvalidasi input
  • mengecek apakah email sudah ada
  • membuat user
  • mengirim welcome email

Ports:

PortTanggung jawab
UserRepositorymencari dan menyimpan user
PasswordHasherhash password
EmailSendermengirim welcome email

Adapters:

AdapterMengimplementasikan
PostgresUserRepositoryUserRepository
BcryptPasswordHasherPasswordHasher
SmtpEmailSenderEmailSender

Logic registrasi tetap stabil meskipun provider email atau database berubah.

Diagram Hexagonal Architecture

Diagram berikut menunjukkan bentuk sederhananya. Application core ada di tengah. Bagian luar hanya masuk lewat input port atau keluar lewat output port.

flowchart LR
  subgraph Outside["Outside World"]
    HTTP["HTTP Controller"]
    CLI["CLI Command"]
    Queue["Message Consumer"]
    DB["PostgreSQL Adapter"]
    SMTP["SMTP Adapter"]
    Hash["Bcrypt Adapter"]
  end

  subgraph Core["Application Core"]
    UseCase["RegisterUser Use Case"]
    UserRepoPort["UserRepository Port"]
    EmailPort["EmailSender Port"]
    HashPort["PasswordHasher Port"]
  end

  HTTP --> UseCase
  CLI --> UseCase
  Queue --> UseCase

  UseCase --> UserRepoPort
  UseCase --> EmailPort
  UseCase --> HashPort

  DB -. implements .-> UserRepoPort
  SMTP -. implements .-> EmailPort
  Hash -. implements .-> HashPort

Yang penting bukan bentuk hexagon-nya secara visual. Yang penting adalah arah dependency: adapter boleh mengenal core, tetapi core tidak perlu mengenal adapter.

Contoh Struktur Folder

Struktur folder bisa berbeda-beda, tapi pembagian seperti ini cukup mudah dipahami:

src/
  application/
    ports/
      UserRepository.ts
      EmailSender.ts
      PasswordHasher.ts
    use-cases/
      RegisterUser.ts
  domain/
    User.ts
  infrastructure/
    database/
      PostgresUserRepository.ts
    email/
      SmtpEmailSender.ts
    security/
      BcryptPasswordHasher.ts
  interfaces/
    http/
      RegisterUserController.ts
  main.ts

application dan domain adalah bagian yang ingin dijaga tetap bersih. infrastructure dan interfaces adalah adapter yang boleh bergantung pada framework, database client, SDK email, atau library hashing.

Contoh Kode: Register User

Mulai dari port. Port mendeskripsikan kebutuhan use case, bukan cara teknis menjalankannya.

export type User = {
  id: string;
  email: string;
  passwordHash: string;
};

export interface UserRepository {
  findByEmail(email: string): Promise<User | null>;
  save(user: User): Promise<void>;
}

export interface PasswordHasher {
  hash(rawPassword: string): Promise<string>;
}

export interface EmailSender {
  sendWelcomeEmail(email: string): Promise<void>;
}

Lalu application service atau use case:

type RegisterUserInput = {
  email: string;
  password: string;
};

export class RegisterUser {
  constructor(
    private readonly users: UserRepository,
    private readonly passwordHasher: PasswordHasher,
    private readonly emailSender: EmailSender,
  ) {}

  async execute(input: RegisterUserInput) {
    if (!input.email.includes("@")) {
      throw new Error("Email tidak valid");
    }

    if (input.password.length < 8) {
      throw new Error("Password minimal 8 karakter");
    }

    const existingUser = await this.users.findByEmail(input.email);

    if (existingUser) {
      throw new Error("Email sudah terdaftar");
    }

    const passwordHash = await this.passwordHasher.hash(input.password);
    const user: User = {
      id: crypto.randomUUID(),
      email: input.email,
      passwordHash,
    };

    await this.users.save(user);
    await this.emailSender.sendWelcomeEmail(user.email);

    return {
      id: user.id,
      email: user.email,
    };
  }
}

Perhatikan bahwa RegisterUser tidak tahu apakah data disimpan di PostgreSQL, MySQL, DynamoDB, atau memory. Ia juga tidak tahu email dikirim lewat SMTP, SendGrid, atau message queue.

Adapter database bisa terlihat seperti ini:

export class PostgresUserRepository implements UserRepository {
  constructor(private readonly db: DatabaseClient) {}

  async findByEmail(email: string): Promise<User | null> {
    const row = await this.db.queryOne(
      "select id, email, password_hash from users where email = $1",
      [email],
    );

    if (!row) {
      return null;
    }

    return {
      id: row.id,
      email: row.email,
      passwordHash: row.password_hash,
    };
  }

  async save(user: User): Promise<void> {
    await this.db.execute(
      "insert into users (id, email, password_hash) values ($1, $2, $3)",
      [user.id, user.email, user.passwordHash],
    );
  }
}

Adapter HTTP menjadi pintu masuk. Ia menerjemahkan request HTTP menjadi input use case.

export class RegisterUserController {
  constructor(private readonly registerUser: RegisterUser) {}

  async handle(req: Request): Promise<Response> {
    const body = await req.json();

    try {
      const result = await this.registerUser.execute({
        email: body.email,
        password: body.password,
      });

      return Response.json(result, { status: 201 });
    } catch (error) {
      return Response.json(
        { message: error instanceof Error ? error.message : "Registration failed" },
        { status: 400 },
      );
    }
  }
}

Controller boleh tahu detail HTTP. Adapter database boleh tahu detail SQL. Tetapi use case tetap berbicara lewat interface yang ia butuhkan.

Composition Root

Satu pertanyaan praktis yang sering muncul: kalau core tidak boleh membuat adapter sendiri, siapa yang menyambungkan semuanya?

Jawabannya adalah composition root, biasanya di entry point aplikasi.

const db = new DatabaseClient(process.env.DATABASE_URL);

const userRepository = new PostgresUserRepository(db);
const passwordHasher = new BcryptPasswordHasher();
const emailSender = new SmtpEmailSender(process.env.SMTP_URL);

const registerUser = new RegisterUser(
  userRepository,
  passwordHasher,
  emailSender,
);

export const registerUserController = new RegisterUserController(registerUser);

Di sinilah object nyata dirakit. Core tetap bersih, tapi aplikasi tetap bisa memakai teknologi nyata.

Kenapa Membantu Testing?

Hexagonal architecture membuat business logic lebih mudah dites karena core bisa diuji tanpa infrastructure asli.

Contoh:

Target testGaya dependency
aturan registrasifake repository dan fake email sender
perilaku HTTPcontroller test
SQL databaserepository integration test

Test jadi lebih fokus. Kamu tidak perlu menjalankan web server dan database asli hanya untuk mengetes business rule.

Contoh unit test untuk use case:

class FakeUserRepository implements UserRepository {
  users = new Map<string, User>();

  async findByEmail(email: string) {
    return this.users.get(email) ?? null;
  }

  async save(user: User) {
    this.users.set(user.email, user);
  }
}

class FakePasswordHasher implements PasswordHasher {
  async hash(rawPassword: string) {
    return `hashed:${rawPassword}`;
  }
}

class FakeEmailSender implements EmailSender {
  sentTo: string[] = [];

  async sendWelcomeEmail(email: string) {
    this.sentTo.push(email);
  }
}

const users = new FakeUserRepository();
const emailSender = new FakeEmailSender();
const useCase = new RegisterUser(
  users,
  new FakePasswordHasher(),
  emailSender,
);

const result = await useCase.execute({
  email: "[email protected]",
  password: "secret123",
});

expect(result.email).toBe("[email protected]");
expect(emailSender.sentTo).toContain("[email protected]");

Test ini tidak butuh web server, database, SMTP server, atau bcrypt asli. Yang diuji adalah aturan registrasi.

Kapan Pattern Ini Layak Dipakai?

Hexagonal architecture paling terasa manfaatnya saat aplikasi punya business rules yang cukup penting dan dependency eksternal yang bisa berubah.

Gunakan saat:

  • use case punya aturan bisnis yang perlu dites serius
  • aplikasi punya beberapa delivery mechanism, misalnya HTTP dan queue
  • dependency eksternal sering diganti atau punya risiko tinggi
  • tim ingin memisahkan domain logic dari framework
  • workflow mulai lebih kompleks daripada CRUD sederhana

Untuk aplikasi CRUD kecil, pattern ini bisa terasa terlalu berat. Tidak apa-apa mulai dari struktur sederhana, lalu menambahkan port saat boundary benar-benar mulai dibutuhkan.

Kesalahan Umum

Membuat terlalu banyak interface

Tidak semua class butuh port. Tambahkan boundary saat replacement, testing, atau ownership memang penting.

Membiarkan tipe framework bocor ke core

Jika use case menerima object HTTP request, core sudah terikat ke framework web.

Mengira architecture hanya soal folder

Folder membantu, tapi arah dependency lebih penting daripada nama folder.

Overengineering aplikasi CRUD kecil

Aplikasi sederhana mungkin tidak butuh struktur hexagonal penuh. Gunakan pattern ini saat kompleksitasnya memang membutuhkan boundary.

Checklist

  • Letakkan business use case dekat dengan core.
  • Jauhkan detail framework dan infrastructure dari core.
  • Definisikan port untuk dependency eksternal yang penting.
  • Implementasikan port lewat adapter.
  • Jaga arah dependency tetap menuju ke dalam.
  • Test core logic dengan fake adapter.
  • Gunakan integration test untuk adapter asli.
  • Hindari interface yang hanya jadi dekorasi.

FAQ

Apakah hexagonal architecture sama dengan clean architecture?

Keduanya berhubungan erat dan sering disandingkan dengan konsep Layered Architecture vs Clean Architecture serta Domain-Driven Design (DDD). Sama-sama menekankan arah dependency dan perlindungan business rules dari detail eksternal. Untuk mengelola transaksi lintas service terdistribusi di luar boundary core, kamu bisa mengombinasikannya dengan Saga Pattern. Istilah dan layering-nya saja yang berbeda.

Apakah setiap repository butuh interface?

Tidak selalu. Tambahkan port saat ia memberi isolasi, fleksibilitas testing, atau independensi teknologi yang nyata.

Apakah frontend bisa memakai hexagonal architecture?

Bisa. Ide yang sama dapat memisahkan UI framework dari business logic, API client, storage, dan domain rules.

Kesimpulan

Hexagonal architecture membantu menjaga core aplikasi tetap independen dari delivery mechanism dan infrastructure.

Gunakan ports untuk menyatakan apa yang dibutuhkan core. Gunakan adapters untuk menghubungkan teknologi nyata. Jaga arah dependency tetap jelas, dan sistem menjadi lebih mudah dites serta diubah.

Apakah kamu sudah menerapkan Hexagonal Architecture atau pola Ports & Adapters di proyekmu? Atau menurutmu abstraknya terasa terlalu rumit untuk skala aplikasi saat ini? Bagikan pengalaman dan pendapatmu di kolom komentar di bawah! 💬

Lanjutkan membaca topik yang masih satu konteks.

Jangan Ketinggalan Info Terbaru

Dapatkan artikel teknologi, tips, dan insights menarik langsung ke email Kamu.