Arsitektur Software: MVC, CQRS, Event-Driven

Foto oleh Unsplash

Foto oleh Unsplash
Pola arsitektur software adalah solusi yang dapat digunakan kembali untuk masalah struktural yang berulang dalam desain software. Pola yang tepat dapat membuat aplikasi Anda lebih mudah diskalakan, ditest, dan dikembangkan. Pola yang salah dapat menciptakan kompleksitas yang tidak perlu yang memperlambat tim Anda selama bertahun-tahun. Postingan ini membandingkan tiga pola terpenting — MVC, CQRS, dan Event-Driven Architecture — dengan contoh TypeScript konkret untuk membantu Anda memilih dan menggabungkannya dengan percaya diri.
Model-View-Controller adalah pola arsitektur yang paling banyak digunakan dalam pengembangan web. Ini memisahkan aplikasi menjadi tiga komponen yang saling terhubung: Model (data dan logika bisnis), View (lapisan presentasi), dan Controller (penanganan request dan koordinasi). Keawetan MVC berasal dari kesederhanaan dan keakrabannya yang luas — hampir setiap web framework mendukungnya, dan sebagian besar developer memahaminya segera.
Next.js modern memetakan secara alami ke konsep MVC bahkan tanpa terminologi MVC yang eksplisit. React Server Component bertindak sebagai View, Route Handler bertindak sebagai Controller, dan lapisan data Anda (model Prisma, kelas service, fungsi repository) bertindak sebagai Model. Co-location UI dan pengambilan data di komponen halaman yang sama di App Router sedikit mengaburkan batas, tetapi pemisahan kepentingan tetap ada.
// MVC in a Next.js App Router context
// Model — src/models/post.ts
export interface Post { id: string; title: string; body: string; authorId: string; }
export async function getPostById(id: string): Promise<Post | null> {
return db.post.findUnique({ where: { id } });
}
// Controller (Route Handler) — app/api/posts/[id]/route.ts
import { getPostById } from "@/models/post";
export async function GET(_: Request, { params }: { params: { id: string } }) {
const post = await getPostById(params.id);
if (!post) return Response.json({ error: "Not found" }, { status: 404 });
return Response.json(post);
}
// View — app/blog/[id]/page.tsx (React Server Component)
import { getPostById } from "@/models/post";
export default async function PostPage({ params }: { params: { id: string } }) {
const post = await getPostById(params.id);
return <article><h1>{post?.title}</h1><p>{post?.body}</p></article>;
}Di Next.js App Router, pilih React Server Component untuk pengambilan data daripada API route handler. RSC menghilangkan satu network round-trip dan mengurangi JavaScript di sisi client. Gunakan Route Handler hanya untuk request yang diinisiasi client (pengiriman form, mutasi dari komponen 'use client').
Command Query Responsibility Segregation memisahkan bagian sistem Anda yang mengubah state (Command) dari bagian yang membaca state (Query). Pemisahan ini memungkinkan Anda mengoptimalkan masing-masing secara independen — read model yang dioptimalkan untuk tampilan UI tertentu dapat memiliki bentuk yang sama sekali berbeda dari write model yang menerapkan aturan bisnis. CQRS powerful tetapi menambah kompleksitas; paling dibenarkan dalam sistem di mana read dan write memiliki kebutuhan scaling yang sangat berbeda.
Command mewakili maksud untuk mengubah state: PlaceOrderCommand, CancelSubscriptionCommand. Ia harus divalidasi dan ditangani oleh handler khusus. Query meminta proyeksi state yang dioptimalkan untuk use case tertentu: GetOrderSummaryQuery mengembalikan DTO datar yang dirancang untuk UI daftar pesanan. Read model dapat berupa SQL view, Redis cache, atau database yang dioptimalkan untuk read — CQRS tidak mengatur implementasinya.
// CQRS: separate read models from write models
// Write side — Command
interface PlaceOrderCommand {
userId: string;
items: { productId: string; qty: number }[];
}
async function handlePlaceOrder(cmd: PlaceOrderCommand): Promise<string> {
const orderId = generateId();
await db.orders.insert({ id: orderId, ...cmd, status: "pending" });
await eventBus.publish("order.placed", { orderId, ...cmd });
return orderId;
}
// Read side — Query (optimized read model, possibly from a separate DB/view)
interface OrderSummaryDTO {
orderId: string;
customerName: string;
itemCount: number;
total: number;
status: string;
}
async function getOrderSummary(orderId: string): Promise<OrderSummaryDTO> {
return readDb.orderSummaries.findOne({ where: { orderId } });
}CQRS sering dipasangkan dengan Event Sourcing, di mana write model menyimpan urutan event (OrderPlaced, ItemShipped, OrderCancelled) daripada state saat ini. Read model dibangun dengan memutar ulang event ini. Ini memberi Anda jejak audit lengkap, kemampuan untuk memutar ulang history guna membangun read model baru, dan integrasi alami dengan Event-Driven Architecture. Trade-off-nya adalah kompleksitas yang jauh lebih tinggi — jangan mengadopsi Event Sourcing tanpa kebutuhan yang meyakinkan.
Event-Driven Architecture (EDA) adalah pola di mana komponen berkomunikasi dengan mengeluarkan dan mengonsumsi event secara asinkron melalui message broker (Kafka, RabbitMQ, AWS EventBridge). EDA memisahkan producer dari consumer — order service menerbitkan event 'order.placed' tanpa mengetahui atau peduli layanan mana yang mengonsumsinya. Ini memungkinkan coupling yang sangat longgar, tetapi memerlukan pemikiran matang tentang evolusi schema event, jaminan urutan, dan penanganan error.
Event adalah fakta yang terjadi di masa lalu ('pesanan telah ditempatkan'). Command adalah permintaan agar sesuatu terjadi ('tempatkan pesanan ini'). Query meminta state saat ini. Event disiarkan — sejumlah consumer dapat bereaksi. Command diarahkan — satu handler bertanggung jawab. Memahami perbedaan ini mencegah kesalahan desain umum seperti menamai event sebagai command.
EDA memperkenalkan eventual consistency — ketika order service menerbitkan event dan inventory service mengonsumsinya, ada jendela di mana inventaris belum diperbarui. Ini dapat diterima untuk banyak use case tetapi berbahaya untuk yang lain (misalnya, menagih pelanggan sebelum mengonfirmasi inventaris). Selalu analisis kebutuhan konsistensi Anda sebelum mengadopsi EDA, dan implementasikan compensating transaction (saga) untuk alur yang memerlukan konsistensi kuat.
Ketika operasi mencakup beberapa layanan — tempatkan pesanan, reservasi inventaris, tagih pembayaran — Anda tidak dapat menggunakan database transaction lintas batas layanan. Pola Saga mengorkestrasikan serangkaian transaksi lokal, setiap langkah menerbitkan event yang memicu langkah berikutnya. Jika ada langkah yang gagal, compensating transaction akan membatalkan langkah sebelumnya. Choreography saga terdesentralisasi; orchestration saga menggunakan koordinator terpusat.
Aplikasi nyata jarang menggunakan satu pola secara terisolasi. Kombinasi umum: MVC untuk lapisan request/response, CQRS untuk memisahkan jalur read dan write untuk domain yang kompleks, dan EDA untuk mengintegrasikan lintas batas layanan dan menangani async workflow. Kuncinya adalah memahami masalah apa yang dipecahkan setiap pola dan menerapkannya secara bedah pada bagian sistem Anda di mana trade-off masuk akal.
Mulai dengan MVC. Tambahkan CQRS ketika read dan write model Anda berbeda secara signifikan atau ketika performa read menjadi bottleneck. Tambahkan EDA ketika Anda perlu memisahkan layanan, menangani async workflow (notifikasi email, background processing), atau mengintegrasikan dengan sistem eksternal. Tambahkan Event Sourcing hanya ketika Anda benar-benar membutuhkan jejak audit atau kemampuan memutar ulang history.
Dokumentasikan keputusan arsitektur signifikan menggunakan Architecture Decision Records (ADR). ADR menangkap konteks (mengapa kita membuat keputusan ini?), opsi yang dipertimbangkan, keputusan yang dibuat, dan konsekuensinya (positif dan negatif). ADR tinggal di repository Anda bersama kode dan memberikan catatan historis yang sangat berharga tentang mengapa sistem Anda seperti itu — konteks penting untuk onboarding engineer baru.
Gunakan Structurizr DSL atau diagram C4 model untuk mendokumentasikan arsitektur Anda di beberapa level abstraksi (sistem, container, komponen). Diagram otomatis sebagai kode mencegah documentation drift dan memberi anggota tim baru peta sistem yang dapat dinavigasi.
Setiap pola arsitektur ada untuk menyelesaikan masalah tertentu. Cocokkan pola dengan masalah, bukan sebaliknya. MVC menyelesaikan pemisahan presentasi, data, dan logika kontrol. CQRS menyelesaikan ketidakcocokan impedansi antara read dan write yang dioptimalkan. EDA menyelesaikan coupling antara komponen yang perlu bereaksi terhadap perubahan state satu sama lain. Jika ragu, mulai lebih sederhana dan berkembang — kompleksitas arsitektur jauh lebih mudah ditambahkan daripada dihapus.
Konsep arsitektur kunci dalam postingan ini meliputi MVC, CQRS, Event Sourcing, EDA, Saga, and eventual consistency.