Episode ini menaikkan level dari kode ke arsitektur: clean architecture, layered architecture, dan modular monolith, mediator dan event-driven design, CQRS dan domain-driven design, serta service composition dan bounded contexts untuk aplikasi enterprise.

Kode yang benar tidak otomatis menjadi sistem yang terawat. Episode 16 membahas arsitektur: cara menyusun project agar mudah diubah, diuji, dan diskalakan saat tim dan fitur bertambah. Kalian akan mengenal clean architecture, layered architecture, modular monolith, CQRS, dan domain-driven design.
Pola-pola ini bukan aturan kaku, melainkan perangkat berpikir. Kuncinya adalah memisahkan apa yang sering berubah (framework, UI, database) dari inti domain yang stabil — dan menjaga arah dependensi agar tidak melingkar.
Layered architecture membagi aplikasi menjadi Presentation, Application, Domain, dan Infrastructure. Clean architecture menegaskan aturan dependensi: lapisan dalam (Domain) tidak boleh bergantung pada lapisan luar. Arah dependensi menuju ke dalam.
Struktur project yang umum:
dotnet new sln -n Order
dotnet new classlib -n Order.Domain -o src/Order.Domain
dotnet new classlib -n Order.Application -o src/Order.Application
dotnet new webapi -n Order.Api -o src/Order.Api
dotnet sln add src/Order.Domain src/Order.Application src/Order.ApiOrder.Domain memuat entitas dan aturan bisnis murni. Order.Application memuat use case dan interface. Order.Api menghubungkan keduanya dengan dunia luar. Referensi dibuat satu arah: Application ke Domain, Api ke Application.
Keuntungannya jelas: domain bisa diuji tanpa database atau web server, dan mengganti UI atau database tidak menyentuh logika bisnis. Biayanya adalah kompleksitas awal yang lebih tinggi — jangan terapkan clean architecture penuh pada aplikasi kecil.
Modular monolith menggabungkan kesederhanaan monolit dengan pemisahan microservice: satu aplikasi yang dideploy, tetapi dipecah menjadi modul yang mandiri dengan boundary tegas.
internal class OrderModule
{
public static void Register(IServiceCollection services)
{
services.AddScoped<IOrderService, OrderService>();
}
}Setiap modul mendaftarkan layanannya sendiri. Modul berkomunikasi lewat interface internal, bukan memanggil implementasi modul lain secara langsung. Ini menjaga otonomi tanpa biaya operasional microservice — strategi yang baik untuk memulai dan memecah nanti.
Mediator memisahkan pemanggil dari handler — mengirim perintah (command) ke handler yang sesuai. Library MediatR adalah implementasi paling populer:
dotnet add package MediatRLalu definisikan command dan handler:
public record BuatPesananCommand(string Produk, int Qty) : IRequest<int>;
public class BuatPesananHandler : IRequestHandler<BuatPesananCommand, int>
{
public Task<int> Handle(BuatPesananCommand request, CancellationToken ct)
{
Console.WriteLine($"{request.Produk} x {request.Qty}");
return Task.FromResult(1);
}
}IMediator.Send(new BuatPesananCommand(...)) meneruskan perintah ke handler yang terdaftar. Kode pengirim tidak tahu handler mana yang menangani — dependensi menjadi longgar dan mudah diganti atau diuji.
Selain command, MediatR mendukung notification untuk event. Saat satu kejadian terjadi, banyak handler dijalankan — misalnya saat pesanan dibuat, kirim email dan update stok. Pengiriman dilakukan dengan IMediator.Publish(event). Ini memisahkan efek samping dari inti logika.
CQRS (Command Query Responsibility Segregation) memisahkan model baca dan tulis. Perintah (command) mengubah state; query membaca tanpa efek samping. Dengan command/query handler yang terpisah, masing-masing bisa diskalakan dan dioptimalkan sendiri.
Domain-Driven Design (DDD) berfokus pada pemodelan domain yang kompleks: entitas dengan identitas, value objects yang immutable, aggregate yang menjaga konsistensi, dan repository yang membungkus persistence. DDD paling berguna saat aturan bisnisnya rumit — bukan untuk CRUD sederhana.
Bounded context membagi domain menjadi subdomain yang terpisah — misalnya Order dan Billing — masing-masing dengan model dan istilahnya sendiri. Komposisi layanan terjadi lewat antarmuka antar context, bukan berbagi class internal.
public interface IBillingClient
{
Task TagihAsync(int pesananId, decimal total);
}
public class BillingClient : IBillingClient
{
private readonly HttpClient _http;
public BillingClient(HttpClient http) => _http = http;
}IBillingClient adalah kontrak antar context yang diekspos oleh modul Billing dan dikonsumsi Order. Saat nanti dipisah menjadi microservice, kontrak ini menjadi API — dengan mengganti implementasi BillingClient dari lokal ke HTTP tanpa mengubah pemakai.
Info
Mulailah sederhana: layered architecture yang rapi lebih baik daripada clean architecture yang setengah jadi. Tambah pola seperti CQRS dan DDD saat kompleksitas domain benar-benar menuntutnya.
Inti yang harus dibawa pulang:
Di episode 17 selanjutnya kita akan membahas messaging dan integration — background dan hosted services, message brokers RabbitMQ, Kafka, dan Azure Service Bus, integrasi gRPC dan REST, serta distributed transactions dan eventual consistency.