Merancang arsitektur aplikasi PHP yang tumbuh: membandingkan modular monolith dan microservices, memahami peran API Gateway dan message queues untuk event-driven, lalu menerapkan DTO, service layer, dan dependency injection container agar kode tetap terstruktur dan mudah diuji.

Sejauh ini kita membangun satu aplikasi. Episode 21 membahas pertanyaan yang muncul saat aplikasi tumbuh besar dan melibatkan banyak tim: bagaimana menata arsitekturnya? Dua ekstrem yang sering dipertentangkan adalah modular monolith dan microservices — dan jawaban yang benar sering kali bukan keduanya secara mentah, melainkan kombinasi yang disengaja.
Mengapa penting? Arsitektur menentukan kemampuan tim untuk bergerak cepat bertahun-tahun ke depan. Microservices yang dipaksakan pada tim kecil justru memperlambat (banyak infra, banyak deployment), sementara monolith tanpa batas modul menjadi sulit dipelihara. Episode ini juga memperkenalkan praktik kode yang menjaga perbatasan arsitektur tetap sehat: DTO, service layer, dan dependency injection.
| Aspek | Modular Monolith | Microservices |
|---|---|---|
| Unit deploy | Satu aplikasi, modul internal | Banyak service, deploy terpisah |
| Komunikasi | Langsung via kode/fungsi | Via HTTP/gRPC/message queue |
| Batas batas | Module dalam satu codebase | Service terpisah, independen |
| Kelebihan | Sederhana, murah, mudah debug | Skala & tim independen, isolasi kegagalan |
| Kekurangan | Batas bisa luntur; scaling seragam | Kompleksitas besar; network failure; debugging sulit |
Aturan praktis industri:
app/Modules/
Auth/
Controllers/ Services/ Repositories/ DTOs/
Order/
Controllers/ Services/ Repositories/ DTOs/
Payment/
Controllers/ Services/ Repositories/ DTOs/Setiap modul punya public API (controller + DTO yang boleh dipakai modul lain) dan internal (yang tidak boleh diakses di luar). Aturan ini dijaga lewat review kode dan tooling.
Saat service sudah terpecah, client butuh satu pintu masuk. API Gateway merutekan, mengautentikasi, membatasi rate, dan mengagregasi response:
Implementasi populer di ekosistem PHP: Laravel API Gateway (package/cloud), atau pasangan Nginx + rate limiting untuk pemisahan yang lebih ringan. Poin kunci: gateway adalah satu titik otentikasi dan satu bentuk kontrak API untuk client.
Saat service berkomunikasi asinkron, message queue menjadi tulang punggung:
| Tool | Karakteristik | Kapan dipakai |
|---|---|---|
| Redis Streams | Cepat, sederhana, dalam ekosistem Laravel | Event internal, workload sedang |
| RabbitMQ | Routing/queue canggih, broker klasik | Enterprise, pola kompleks |
| Apache Kafka | Log berdurasi panjang, throughput raksasa | Streaming/event-sourcing skala besar |
namespace App\Events;
use App\Models\Order;
class OrderPaid
{
public function __construct(public Order $order) {}
}event(new OrderPaid($order)); // publish
// didengarkan oleh listener yang menangani
// notifikasi, email, stok, dsb. — kadang lewat queue (episode 20)
class KirimNotifikasiOrder implements ShouldQueue
{
public function handle(OrderPaid $event): void { /* ... */ }
}Pola event-driven memisahkan modul: modul Order tidak tahu menahu soal notifikasi atau email — ia hanya mem-publish event. Ini menjaga coupling antar modul tetap rendah.
Tip
Kencangkan event sebagai kontrak antar modul: nama event dan bentuk payload-nya adalah public API. Bila dua modul mulai membaca properti internal satu sama lain, batas modul sudah bocor — refactor sebelum terlambat.
DTO adalah object immutable yang membawa data antar lapisan — pengganti array mentah yang tidak bertipe:
namespace App\Modules\Order\DTOs;
final readonly class CreateOrderDTO
{
public function __construct(
public int $userId,
public int $productId,
public int $qty,
public float $total,
) {}
public static function fromRequest(array $input): self
{
return new self(
userId: (int) $input["user_id"],
productId: (int) $input["product_id"],
qty: max(1, (int) $input["qty"]),
total: (float) $input["total"],
);
}
}Kelebihan DTO: type-safe (bukan array yang bisa berisi apa pun), immutable (readonly), self-documenting, dan mudah ditest. Ini pilar praktik "Poka-Yoke" kode — kesalahan terdeteksi saat kompilasi/analisis statis, bukan saat produksi.
Service layer memegang logika bisnis; controller hanya menerima request dan mengembalikan response:
namespace App\Modules\Order\Services;
use App\Modules\Order\DTOs\CreateOrderDTO;
final class OrderService
{
public function __construct(
private OrderRepository $repo,
private PaymentGateway $payment,
) {}
public function create(CreateOrderDTO $dto): Order
{
if ($dto->qty <= 0) {
throw new InvalidQuantityException();
}
return $this->payment->process($dto)
? $this->repo->create($dto)
: throw new PaymentFailedException();
}
}Controller memanggil service — sekali lagi kita melihat pola dari episode 13 dan 8 dipadukan. Service yang tipis dan fokus membuat unit test langsung menargetkan logika bisnis tanpa perlu HTTP (episode 10).
DI Container adalah mesin yang menciptakan dan menyuntikkan dependensi secara otomatis:
$orderService = new OrderService(
new OrderRepository($pdo),
new PaymentGateway(),
);// Laravel: otomatis me-resolve constructor dependency
$service = app(OrderService::class);
// Symfony: definisi eksplisit di services.yaml
// services:
// App\Modules\Order\Services\OrderService: ~Manfaat DI container:
OrderService karena tahu cara membuat semua dependensinya.Danger
Jangan membuat Service Locator (mengambil dependensi dari container di dalam service) — itu menyembunyikan dependensi dan merusak keterujian. Pola benar: constructor injection — semua dependensi terlihat jelas di tanda tangan konstruktor.
Inti yang harus dibawa pulang:
Di episode 22 — episode terakhir series ini — kita menarik kesimpulan besar: Ekosistem, Alternatif & Refleksi Akhir — membandingkan PHP dengan Node.js, Python, dan Java, kapan memilih yang mana, lalu rekap lengkap episode 0-21 dan checklist production untuk aplikasi PHP kalian.